jigsawpuzzlegame 1.0.6 → 1.0.8

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.
Files changed (3) hide show
  1. package/README.md +41 -19
  2. package/jigsaw-puzzle-game.js +1998 -1960
  3. package/package.json +1 -1
@@ -1,1961 +1,1999 @@
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
- this.rotationDegrees = 0; // Cumulative rotation in degrees for smooth animation
398
- }
399
-
400
- merge(otherPoly) {
401
- const puzzle = this.puzzle;
402
- const orgpckxmin = this.pckxmin;
403
- const orgpckymin = this.pckymin;
404
- const pbefore = getTransformed(
405
- 0,
406
- 0,
407
- this.nx * puzzle.scalex,
408
- this.ny * puzzle.scaley,
409
- this.rot
410
- );
411
-
412
- const kOther = puzzle.polyPieces.indexOf(otherPoly);
413
- puzzle.polyPieces.splice(kOther, 1);
414
- puzzle.container.removeChild(otherPoly.canvas);
415
-
416
- for (let k = 0; k < otherPoly.pieces.length; ++k) {
417
- this.pieces.push(otherPoly.pieces[k]);
418
- if (otherPoly.pieces[k].kx < this.pckxmin)
419
- this.pckxmin = otherPoly.pieces[k].kx;
420
- if (otherPoly.pieces[k].kx + 1 > this.pckxmax)
421
- this.pckxmax = otherPoly.pieces[k].kx + 1;
422
- if (otherPoly.pieces[k].ky < this.pckymin)
423
- this.pckymin = otherPoly.pieces[k].ky;
424
- if (otherPoly.pieces[k].ky + 1 > this.pckymax)
425
- this.pckymax = otherPoly.pieces[k].ky + 1;
426
- }
427
-
428
- this.pieces.sort(function (p1, p2) {
429
- if (p1.ky < p2.ky) return -1;
430
- if (p1.ky > p2.ky) return 1;
431
- if (p1.kx < p2.kx) return -1;
432
- if (p1.kx > p2.kx) return 1;
433
- return 0;
434
- });
435
-
436
- this.listLoops();
437
- this.drawImage();
438
-
439
- const pafter = getTransformed(
440
- puzzle.scalex * (orgpckxmin - this.pckxmin),
441
- puzzle.scaley * (orgpckymin - this.pckymin),
442
- puzzle.scalex * (this.pckxmax - this.pckxmin + 1),
443
- puzzle.scaley * (this.pckymax - this.pckymin + 1),
444
- this.rot
445
- );
446
-
447
- this.moveTo(this.x - pafter.x + pbefore.x, this.y - pafter.y + pbefore.y);
448
- puzzle.evaluateZIndex();
449
-
450
- function getTransformed(orgx, orgy, width, height, rot) {
451
- const dx = orgx - width / 2;
452
- const dy = orgy - height / 2;
453
- return {
454
- x: width / 2 + [1, 0, -1, 0][rot] * dx + [0, -1, 0, 1][rot] * dy,
455
- y: height / 2 + [0, 1, 0, -1][rot] * dx + [1, 0, -1, 0][rot] * dy
456
- };
457
- }
458
- }
459
-
460
- ifNear(otherPoly) {
461
- const puzzle = this.puzzle;
462
- if (this.rot != otherPoly.rot) return false;
463
-
464
- let p1, p2;
465
- let org = this.getOrgP();
466
- let orgOther = otherPoly.getOrgP();
467
-
468
- if (mhypot(org.x - orgOther.x, org.y - orgOther.y) >= puzzle.dConnect)
469
- return false;
470
-
471
- for (let k = this.pieces.length - 1; k >= 0; --k) {
472
- p1 = this.pieces[k];
473
- for (let ko = otherPoly.pieces.length - 1; ko >= 0; --ko) {
474
- p2 = otherPoly.pieces[ko];
475
- if (p1.kx == p2.kx && mabs(p1.ky - p2.ky) == 1) return true;
476
- if (p1.ky == p2.ky && mabs(p1.kx - p2.kx) == 1) return true;
477
- }
478
- }
479
-
480
- return false;
481
- }
482
-
483
- listLoops() {
484
- const that = this;
485
- function edgeIsCommon(kx, ky, edge) {
486
- let k;
487
- switch (edge) {
488
- case 0: ky--; break;
489
- case 1: kx++; break;
490
- case 2: ky++; break;
491
- case 3: kx--; break;
492
- }
493
- for (k = 0; k < that.pieces.length; k++) {
494
- if (kx == that.pieces[k].kx && ky == that.pieces[k].ky) return true;
495
- }
496
- return false;
497
- }
498
-
499
- function edgeIsInTbEdges(kx, ky, edge) {
500
- let k;
501
- for (k = 0; k < tbEdges.length; k++) {
502
- if (
503
- kx == tbEdges[k].kx &&
504
- ky == tbEdges[k].ky &&
505
- edge == tbEdges[k].edge
506
- )
507
- return k;
508
- }
509
- return false;
510
- }
511
-
512
- let tbLoops = [];
513
- let tbEdges = [];
514
- let k;
515
- let kEdge;
516
- let lp;
517
- let currEdge;
518
- let tries;
519
- let edgeNumber;
520
- let potNext;
521
-
522
- let tbTries = [
523
- [
524
- { dkx: 0, dky: 0, edge: 1 },
525
- { dkx: 1, dky: 0, edge: 0 },
526
- { dkx: 1, dky: -1, edge: 3 }
527
- ],
528
- [
529
- { dkx: 0, dky: 0, edge: 2 },
530
- { dkx: 0, dky: 1, edge: 1 },
531
- { dkx: 1, dky: 1, edge: 0 }
532
- ],
533
- [
534
- { dkx: 0, dky: 0, edge: 3 },
535
- { dkx: -1, dky: 0, edge: 2 },
536
- { dkx: -1, dky: 1, edge: 1 }
537
- ],
538
- [
539
- { dkx: 0, dky: 0, edge: 0 },
540
- { dkx: 0, dky: -1, edge: 3 },
541
- { dkx: -1, dky: -1, edge: 2 }
542
- ]
543
- ];
544
-
545
- for (k = 0; k < this.pieces.length; k++) {
546
- for (kEdge = 0; kEdge < 4; kEdge++) {
547
- if (!edgeIsCommon(this.pieces[k].kx, this.pieces[k].ky, kEdge))
548
- tbEdges.push({
549
- kx: this.pieces[k].kx,
550
- ky: this.pieces[k].ky,
551
- edge: kEdge,
552
- kp: k
553
- });
554
- }
555
- }
556
-
557
- while (tbEdges.length > 0) {
558
- lp = [];
559
- currEdge = tbEdges[0];
560
- lp.push(currEdge);
561
- tbEdges.splice(0, 1);
562
- do {
563
- for (tries = 0; tries < 3; tries++) {
564
- potNext = tbTries[currEdge.edge][tries];
565
- edgeNumber = edgeIsInTbEdges(
566
- currEdge.kx + potNext.dkx,
567
- currEdge.ky + potNext.dky,
568
- potNext.edge
569
- );
570
- if (edgeNumber === false) continue;
571
- currEdge = tbEdges[edgeNumber];
572
- lp.push(currEdge);
573
- tbEdges.splice(edgeNumber, 1);
574
- break;
575
- }
576
- if (edgeNumber === false) break;
577
- } while (1);
578
- tbLoops.push(lp);
579
- }
580
-
581
- this.tbLoops = tbLoops.map((loop) =>
582
- loop.map((edge) => {
583
- let cell = this.pieces[edge.kp];
584
- if (edge.edge == 0) return cell.ts;
585
- if (edge.edge == 1) return cell.rs;
586
- if (edge.edge == 2) return cell.bs;
587
- return cell.ls;
588
- })
589
- );
590
- }
591
-
592
- getRect() {
593
- const puzzle = this.puzzle;
594
- let rect0 = puzzle.container.getBoundingClientRect();
595
- let rect = this.canvas.getBoundingClientRect();
596
- return {
597
- x: rect.x - rect0.x,
598
- y: rect.y - rect0.y,
599
- right: rect.right - rect0.x,
600
- bottom: rect.bottom - rect0.y,
601
- width: rect.width,
602
- height: rect.height
603
- };
604
- }
605
-
606
- getOrgP() {
607
- const puzzle = this.puzzle;
608
- const rect = this.getRect();
609
- switch (this.rot) {
610
- case 0:
611
- return {
612
- x: rect.x - puzzle.scalex * this.pckxmin,
613
- y: rect.y - puzzle.scaley * this.pckymin
614
- };
615
- case 1:
616
- return {
617
- x: rect.right + puzzle.scaley * this.pckymin,
618
- y: rect.y - puzzle.scalex * this.pckxmin
619
- };
620
- case 2:
621
- return {
622
- x: rect.right + puzzle.scalex * this.pckxmin,
623
- y: rect.bottom + puzzle.scaley * this.pckymin
624
- };
625
- case 3:
626
- return {
627
- x: rect.x - puzzle.scaley * this.pckymin,
628
- y: rect.bottom + puzzle.scalex * this.pckxmin
629
- };
630
- }
631
- }
632
-
633
- drawPath(ctx, shiftx, shifty) {
634
- this.tbLoops.forEach((loop) => {
635
- let without = false;
636
- loop.forEach((side) => {
637
- side.drawPath(ctx, shiftx, shifty, without);
638
- without = true;
639
- });
640
- ctx.closePath();
641
- });
642
- }
643
-
644
- drawImage(special) {
645
- const puzzle = this.puzzle;
646
- this.nx = this.pckxmax - this.pckxmin + 1;
647
- this.ny = this.pckymax - this.pckymin + 1;
648
- this.canvas.width = this.nx * puzzle.scalex;
649
- this.canvas.height = this.ny * puzzle.scaley;
650
-
651
- this.offsx = (this.pckxmin - 0.5) * puzzle.scalex;
652
- this.offsy = (this.pckymin - 0.5) * puzzle.scaley;
653
-
654
- this.path = new Path2D();
655
- this.drawPath(this.path, -this.offsx, -this.offsy);
656
-
657
- this.ctx.fillStyle = "none";
658
- this.ctx.shadowColor = this.selected
659
- ? special
660
- ? "lime"
661
- : "gold"
662
- : "rgba(0, 0, 0, 0.5)";
663
- this.ctx.shadowBlur = this.selected ? mmin(8, puzzle.scalex / 10) : 4;
664
- this.ctx.shadowOffsetX = this.selected ? 0 : -4;
665
- this.ctx.shadowOffsetY = this.selected ? 0 : 4;
666
- this.ctx.fill(this.path);
667
- if (this.selected) {
668
- for (let i = 0; i < 6; i++) this.ctx.fill(this.path);
669
- }
670
- this.ctx.shadowColor = "rgba(0, 0, 0, 0)";
671
-
672
- this.pieces.forEach((pp) => {
673
- this.ctx.save();
674
-
675
- const path = new Path2D();
676
- const shiftx = -this.offsx;
677
- const shifty = -this.offsy;
678
- pp.ts.drawPath(path, shiftx, shifty, false);
679
- pp.rs.drawPath(path, shiftx, shifty, true);
680
- pp.bs.drawPath(path, shiftx, shifty, true);
681
- pp.ls.drawPath(path, shiftx, shifty, true);
682
- path.closePath();
683
-
684
- this.ctx.clip(path);
685
- const srcx = pp.kx ? (pp.kx - 0.5) * puzzle.scalex : 0;
686
- const srcy = pp.ky ? (pp.ky - 0.5) * puzzle.scaley : 0;
687
-
688
- const destx =
689
- (pp.kx ? 0 : puzzle.scalex / 2) +
690
- (pp.kx - this.pckxmin) * puzzle.scalex;
691
- const desty =
692
- (pp.ky ? 0 : puzzle.scaley / 2) +
693
- (pp.ky - this.pckymin) * puzzle.scaley;
694
-
695
- let w = 2 * puzzle.scalex;
696
- let h = 2 * puzzle.scaley;
697
- if (srcx + w > puzzle.gameCanvas.width)
698
- w = puzzle.gameCanvas.width - srcx;
699
- if (srcy + h > puzzle.gameCanvas.height)
700
- h = puzzle.gameCanvas.height - srcy;
701
-
702
- this.ctx.drawImage(
703
- puzzle.gameCanvas,
704
- srcx,
705
- srcy,
706
- w,
707
- h,
708
- destx,
709
- desty,
710
- w,
711
- h
712
- );
713
- this.ctx.lineWidth = puzzle.embossThickness * 1.5;
714
-
715
- this.ctx.translate(
716
- puzzle.embossThickness / 2,
717
- -puzzle.embossThickness / 2
718
- );
719
- this.ctx.strokeStyle = "rgba(0, 0, 0, 0.35)";
720
- this.ctx.stroke(path);
721
-
722
- this.ctx.translate(-puzzle.embossThickness, puzzle.embossThickness);
723
- this.ctx.strokeStyle = "rgba(255, 255, 255, 0.35)";
724
- this.ctx.stroke(path);
725
-
726
- this.ctx.restore();
727
- this.canvas.style.transform = `rotate(${this.rotationDegrees}deg)`;
728
- });
729
- }
730
-
731
- moveTo(x, y) {
732
- this.x = x;
733
- this.y = y;
734
- this.canvas.style.left = x + "px";
735
- this.canvas.style.top = y + "px";
736
- }
737
-
738
- moveToInitialPlace() {
739
- const puzzle = this.puzzle;
740
- this.moveTo(
741
- puzzle.offsx + (this.pckxmin - 0.5) * puzzle.scalex,
742
- puzzle.offsy + (this.pckymin - 0.5) * puzzle.scaley
743
- );
744
- }
745
-
746
- rotate(angle) {
747
- // Calculate the change in rotation
748
- const oldRot = this.rot;
749
- this.rot = angle;
750
-
751
- // Always rotate forward (add 90 degrees for each increment)
752
- // If we're wrapping from 3 to 0, we need to go 270->360 instead of 270->0
753
- if (this.rot === 0 && oldRot === 3) {
754
- // Going from 270° (rot=3) to 0° (rot=0), add 90° to go to 360°
755
- this.rotationDegrees += 90;
756
- } else {
757
- // Normal case: calculate the difference and add it
758
- const diff = this.rot - oldRot;
759
- this.rotationDegrees += diff * 90;
760
- }
761
- }
762
-
763
- isPointInPath(p) {
764
- let npath = new Path2D();
765
- this.drawPath(npath, 0, 0);
766
- let rect = this.getRect();
767
-
768
- let pRefx = [rect.x, rect.right, rect.right, rect.x][this.rot];
769
- let pRefy = [rect.y, rect.y, rect.bottom, rect.bottom][this.rot];
770
-
771
- let mposx =
772
- [1, 0, -1, 0][this.rot] * (p.x - pRefx) +
773
- [0, 1, 0, -1][this.rot] * (p.y - pRefy);
774
- let mposy =
775
- [0, -1, 0, 1][this.rot] * (p.x - pRefx) +
776
- [1, 0, -1, 0][this.rot] * (p.y - pRefy);
777
-
778
- return this.ctx.isPointInPath(this.path, mposx, mposy);
779
- }
780
-
781
- coerceToContainer() {
782
- const puzzle = this.puzzle;
783
- let dimx = [puzzle.scalex, puzzle.scaley, puzzle.scalex, puzzle.scaley][
784
- this.rot
785
- ];
786
- let dimy = [puzzle.scaley, puzzle.scalex, puzzle.scaley, puzzle.scalex][
787
- this.rot
788
- ];
789
- const rect = this.getRect();
790
- if (rect.y > -dimy && rect.bottom < puzzle.contHeight + dimy) {
791
- if (rect.right < dimx) {
792
- this.moveTo(this.x + dimx - rect.right, this.y);
793
- return;
794
- }
795
- if (rect.x > puzzle.contWidth - dimx) {
796
- this.moveTo(this.x + puzzle.contWidth - dimx - rect.x, this.y);
797
- return;
798
- }
799
- return;
800
- }
801
- if (rect.x > -dimx && rect.right < puzzle.contHeight + dimy) {
802
- if (rect.bottom < dimy) {
803
- this.moveTo(this.x, this.y + dimy - rect.bottom);
804
- return;
805
- }
806
- if (rect.y > puzzle.contHeight - dimy) {
807
- this.moveTo(this.x, this.y + puzzle.contHeight - dimy - rect.y);
808
- return;
809
- }
810
- return;
811
- }
812
- if (rect.y < -dimy) {
813
- this.moveTo(this.x, this.y - rect.y - dimy);
814
- this.getRect();
815
- }
816
- if (rect.bottom > puzzle.contHeight + dimy) {
817
- this.moveTo(this.x, this.y + puzzle.contHeight + dimy - rect.bottom);
818
- this.getRect();
819
- }
820
- if (rect.right < dimx) {
821
- this.moveTo(this.x + dimx - rect.right, this.y);
822
- return;
823
- }
824
- if (rect.x > puzzle.contWidth - dimx) {
825
- this.moveTo(this.x + puzzle.contWidth - dimx - rect.x, this.y);
826
- return;
827
- }
828
- }
829
- }
830
-
831
- // ============================================================================
832
- // Helper Functions
833
- // ============================================================================
834
-
835
- function fitImage(img, width, height) {
836
- let wn = img.naturalWidth;
837
- let hn = img.naturalHeight;
838
- let w = width;
839
- let h = (w * hn) / wn;
840
- if (h > height) {
841
- h = height;
842
- w = (h * wn) / hn;
843
- }
844
- img.style.position = "absolute";
845
- img.style.width = w + "px";
846
- img.style.height = h + "px";
847
- img.style.top = "50%";
848
- img.style.left = "50%";
849
- img.style.transform = "translate(-50%,-50%)";
850
- }
851
-
852
- // ============================================================================
853
- // Internal Puzzle Class (refactored to be UI-independent)
854
- // ============================================================================
855
-
856
- class InternalPuzzle {
857
- constructor(container) {
858
- this.container = typeof container === "string"
859
- ? document.getElementById(container)
860
- : container;
861
-
862
- this.gameCanvas = document.createElement("CANVAS");
863
- this.container.appendChild(this.gameCanvas);
864
-
865
- this.srcImage = new Image();
866
- this.imageLoaded = false;
867
-
868
- // State
869
- this.nbPieces = 20;
870
- this.rotationAllowed = false;
871
- this.typeOfShape = 0;
872
- }
873
-
874
- getContainerSize() {
875
- let styl = window.getComputedStyle(this.container);
876
- this.contWidth = parseFloat(styl.width);
877
- this.contHeight = parseFloat(styl.height);
878
- }
879
-
880
- create(baseData) {
881
- this.prng = mMash(baseData ? baseData[3] : null);
882
- this.container.innerHTML = "";
883
- this.getContainerSize();
884
-
885
- if (baseData) {
886
- this.nx = baseData[0];
887
- this.ny = baseData[1];
888
- // baseData[2] is total game width (scalex * nx), not scalex itself
889
- // scalex will be calculated in doScale()
890
- this.rotationAllowed = !!baseData[4];
891
- this.typeOfShape = baseData[5];
892
- } else {
893
- this.computenxAndny();
894
- }
895
-
896
- this.relativeHeight =
897
- this.srcImage.naturalHeight /
898
- this.ny /
899
- (this.srcImage.naturalWidth / this.nx);
900
-
901
- if (!baseData) {
902
- this.typeOfShape = this.typeOfShape || 0;
903
- }
904
-
905
- this.defineShapes({
906
- coeffDecentr: 0.12,
907
- twistf: [twist0, twist1, twist2, twist3][this.typeOfShape]
908
- });
909
-
910
- this.polyPieces = [];
911
- if (!baseData) {
912
- this.pieces.forEach((row) =>
913
- row.forEach((piece) => {
914
- this.polyPieces.push(new PolyPiece(piece, this));
915
- })
916
- );
917
- arrayShuffle(this.polyPieces);
918
- if (this.rotationAllowed)
919
- this.polyPieces.forEach((pp) => {
920
- pp.rot = intAlea(4);
921
- pp.rotationDegrees = pp.rot * 90;
922
- });
923
- } else {
924
- const pps = baseData[8];
925
- const offs = this.rotationAllowed ? 3 : 2;
926
- pps.forEach((ppData) => {
927
- let polyp = new PolyPiece(this.pieces[ppData[offs + 1]][ppData[offs]], this);
928
- polyp.x = ppData[0];
929
- polyp.y = ppData[1];
930
- polyp.rot = this.rotationAllowed ? ppData[2] : 0;
931
- polyp.rotationDegrees = polyp.rot * 90;
932
- for (let k = offs + 2; k < ppData.length; k += 2) {
933
- let kx = ppData[k];
934
- let ky = ppData[k + 1];
935
- polyp.pieces.push(this.pieces[ky][kx]);
936
- polyp.pckxmin = mmin(polyp.pckxmin, kx);
937
- polyp.pckxmax = mmax(polyp.pckxmax, kx + 1);
938
- polyp.pckymin = mmin(polyp.pckymin, ky);
939
- polyp.pckymax = mmax(polyp.pckymax, ky + 1);
940
- }
941
- polyp.listLoops();
942
- this.polyPieces.push(polyp);
943
- });
944
- }
945
- this.evaluateZIndex();
946
- }
947
-
948
- computenxAndny() {
949
- let kx, ky,
950
- width = this.srcImage.naturalWidth,
951
- height = this.srcImage.naturalHeight,
952
- npieces = this.nbPieces;
953
- let err, errmin = 1e9;
954
- let ncv, nch;
955
-
956
- let nHPieces = mround(msqrt((npieces * width) / height));
957
- let nVPieces = mround(npieces / nHPieces);
958
-
959
- for (ky = 0; ky < 5; ky++) {
960
- ncv = nVPieces + ky - 2;
961
- for (kx = 0; kx < 5; kx++) {
962
- nch = nHPieces + kx - 2;
963
- err = (nch * height) / ncv / width;
964
- err = err + 1 / err - 2;
965
- err += mabs(1 - (nch * ncv) / npieces);
966
-
967
- if (err < errmin) {
968
- errmin = err;
969
- this.nx = nch;
970
- this.ny = ncv;
971
- }
972
- }
973
- }
974
- }
975
-
976
- defineShapes(shapeDesc) {
977
- let { coeffDecentr, twistf } = shapeDesc;
978
- const corners = [];
979
- const nx = this.nx, ny = this.ny;
980
- let np;
981
-
982
- for (let ky = 0; ky <= ny; ++ky) {
983
- corners[ky] = [];
984
- for (let kx = 0; kx <= nx; ++kx) {
985
- corners[ky][kx] = new Point(
986
- kx + this.prng.alea(-coeffDecentr, coeffDecentr),
987
- ky + this.prng.alea(-coeffDecentr, coeffDecentr)
988
- );
989
- if (kx == 0) corners[ky][kx].x = 0;
990
- if (kx == nx) corners[ky][kx].x = nx;
991
- if (ky == 0) corners[ky][kx].y = 0;
992
- if (ky == ny) corners[ky][kx].y = ny;
993
- }
994
- }
995
-
996
- this.pieces = [];
997
- for (let ky = 0; ky < ny; ++ky) {
998
- this.pieces[ky] = [];
999
- for (let kx = 0; kx < nx; ++kx) {
1000
- this.pieces[ky][kx] = np = new Piece(kx, ky);
1001
- if (ky == 0) {
1002
- np.ts.points = [corners[ky][kx], corners[ky][kx + 1]];
1003
- np.ts.type = "d";
1004
- } else {
1005
- np.ts = this.pieces[ky - 1][kx].bs.reversed();
1006
- }
1007
- np.rs.points = [corners[ky][kx + 1], corners[ky + 1][kx + 1]];
1008
- np.rs.type = "d";
1009
- if (kx < nx - 1) {
1010
- if (this.prng.intAlea(2))
1011
- twistf(np.rs, corners[ky][kx], corners[ky + 1][kx], this.prng);
1012
- else twistf(np.rs, corners[ky][kx + 2], corners[ky + 1][kx + 2], this.prng);
1013
- }
1014
- if (kx == 0) {
1015
- np.ls.points = [corners[ky + 1][kx], corners[ky][kx]];
1016
- np.ls.type = "d";
1017
- } else {
1018
- np.ls = this.pieces[ky][kx - 1].rs.reversed();
1019
- }
1020
- np.bs.points = [corners[ky + 1][kx + 1], corners[ky + 1][kx]];
1021
- np.bs.type = "d";
1022
- if (ky < ny - 1) {
1023
- if (this.prng.intAlea(2))
1024
- twistf(np.bs, corners[ky][kx + 1], corners[ky][kx], this.prng);
1025
- else twistf(np.bs, corners[ky + 2][kx + 1], corners[ky + 2][kx], this.prng);
1026
- }
1027
- }
1028
- }
1029
- }
1030
-
1031
- scale() {
1032
- const maxWidth = 0.95 * this.contWidth;
1033
- const maxHeight = 0.95 * this.contHeight;
1034
- const woh = this.srcImage.naturalWidth / this.srcImage.naturalHeight;
1035
- let bestWidth = 0;
1036
- let piecex, piecey;
1037
-
1038
- let xtra = this.nx * this.ny * 1.2;
1039
- for (let extrax = 0; extrax <= mceil(this.nx * 0.2); ++extrax) {
1040
- let availx = extrax == 0 ? maxWidth : this.contWidth;
1041
- for (let extray = 0; extray <= mceil(this.ny * 0.2); ++extray) {
1042
- if ((this.nx + extrax) * (this.ny + extray) < xtra) continue;
1043
- let availy = extray == 0 ? maxHeight : this.contHeight;
1044
- piecex = availx / (this.nx + extrax);
1045
- piecey = (piecex * this.nx) / woh / this.ny;
1046
- if (piecey * (this.ny + extray) > availy) {
1047
- piecey = availy / (this.ny + extray);
1048
- piecex = (piecey * this.ny * woh) / this.nx;
1049
- }
1050
- if (piecex * this.nx > bestWidth) bestWidth = piecex * this.nx;
1051
- }
1052
- }
1053
-
1054
- this.doScale(bestWidth);
1055
- }
1056
-
1057
- doScale(width) {
1058
- this.gameWidth = width;
1059
- this.gameHeight = (width * this.srcImage.naturalHeight) / this.srcImage.naturalWidth;
1060
-
1061
- this.gameCanvas.width = this.gameWidth;
1062
- this.gameCanvas.height = this.gameHeight;
1063
- this.gameCtx = this.gameCanvas.getContext("2d");
1064
- this.gameCtx.drawImage(this.srcImage, 0, 0, this.gameWidth, this.gameHeight);
1065
-
1066
- this.gameCanvas.classList.add("gameCanvas");
1067
- this.gameCanvas.style.zIndex = 500;
1068
-
1069
- this.scalex = this.gameWidth / this.nx;
1070
- this.scaley = this.gameHeight / this.ny;
1071
-
1072
- this.pieces.forEach((row) => {
1073
- row.forEach((piece) => piece.scale(this));
1074
- });
1075
-
1076
- this.offsx = (this.contWidth - this.gameWidth) / 2;
1077
- this.offsy = (this.contHeight - this.gameHeight) / 2;
1078
- this.dConnect = mmax(10, mmin(this.scalex, this.scaley) / 10);
1079
- this.embossThickness = mmin(2 + (this.scalex / 200) * (5 - 2), 5);
1080
- }
1081
-
1082
- sweepBy(dx, dy) {
1083
- this.polyPieces.forEach((pp) => {
1084
- pp.moveTo(pp.x + dx, pp.y + dy);
1085
- });
1086
- }
1087
-
1088
- zoomBy(coef, center) {
1089
- let futWidth = this.gameWidth * coef;
1090
- let futHeight = this.gameHeight * coef;
1091
- if (
1092
- ((futWidth > 3000 || futHeight > 3000) && coef > 1) ||
1093
- (futWidth < 200 || futHeight < 200) & (coef < 1)
1094
- )
1095
- return;
1096
- if (coef == 1) return;
1097
-
1098
- this.doScale(futWidth);
1099
- this.polyPieces.forEach((pp) => {
1100
- pp.moveTo(
1101
- coef * (pp.x - center.x) + center.x,
1102
- coef * (pp.y - center.y) + center.y
1103
- );
1104
- pp.drawImage();
1105
- });
1106
- }
1107
-
1108
- relativeMouseCoordinates(event) {
1109
- const br = this.container.getBoundingClientRect();
1110
- return {
1111
- x: event.clientX - br.x,
1112
- y: event.clientY - br.y
1113
- };
1114
- }
1115
-
1116
- limitRectangle(rect) {
1117
- let minscale = mmin(this.scalex, this.scaley);
1118
- rect.x0 = mmin(mmax(rect.x0, -minscale / 2), this.contWidth - 1.5 * minscale);
1119
- rect.x1 = mmin(mmax(rect.x1, -minscale / 2), this.contWidth - 1.5 * minscale);
1120
- rect.y0 = mmin(mmax(rect.y0, -minscale / 2), this.contHeight - 1.5 * minscale);
1121
- rect.y1 = mmin(mmax(rect.y1, -minscale / 2), this.contHeight - 1.5 * minscale);
1122
- }
1123
-
1124
- spreadInRectangle(rect) {
1125
- this.limitRectangle(rect);
1126
- this.polyPieces.forEach((pp) =>
1127
- pp.moveTo(alea(rect.x0, rect.x1), alea(rect.y0, rect.y1))
1128
- );
1129
- }
1130
-
1131
- spreadSetInRectangle(set, rect) {
1132
- this.limitRectangle(rect);
1133
- set.forEach((pp) =>
1134
- pp.moveTo(alea(rect.x0, rect.x1), alea(rect.y0, rect.y1))
1135
- );
1136
- }
1137
-
1138
- optimInitial() {
1139
- const minx = -this.scalex / 2;
1140
- const miny = -this.scaley / 2;
1141
- const maxx = this.contWidth - 1.5 * this.scalex;
1142
- const maxy = this.contHeight - 1.5 * this.scaley;
1143
- let freex = this.contWidth - this.gameWidth;
1144
- let freey = this.contHeight - this.gameHeight;
1145
-
1146
- let where = [0, 0, 0, 0];
1147
- let rects = [];
1148
- if (freex > 1.5 * this.scalex) {
1149
- where[1] = 1;
1150
- rects[1] = {
1151
- x0: this.gameWidth - 0.5 * this.scalex,
1152
- x1: maxx,
1153
- y0: miny,
1154
- y1: maxy
1155
- };
1156
- }
1157
- if (freex > 3 * this.scalex) {
1158
- where[3] = 1;
1159
- rects[3] = {
1160
- x0: minx,
1161
- x1: freex / 2 - 1.5 * this.scalex,
1162
- y0: miny,
1163
- y1: maxy
1164
- };
1165
- rects[1].x0 = this.contWidth - freex / 2 - 0.5 * this.scalex;
1166
- }
1167
- if (freey > 1.5 * this.scaley) {
1168
- where[2] = 1;
1169
- rects[2] = {
1170
- x0: minx,
1171
- x1: maxx,
1172
- y0: this.gameHeight - 0.5 * this.scaley,
1173
- y1: this.contHeight - 1.5 * this.scaley
1174
- };
1175
- }
1176
- if (freey > 3 * this.scaley) {
1177
- where[0] = 1;
1178
- rects[0] = {
1179
- x0: minx,
1180
- x1: maxx,
1181
- y0: miny,
1182
- y1: freey / 2 - 1.5 * this.scaley
1183
- };
1184
- rects[2].y0 = this.contHeight - freey / 2 - 0.5 * this.scaley;
1185
- }
1186
- if (where.reduce((sum, a) => sum + a) < 2) {
1187
- if (freex - freey > 0.2 * this.scalex || where[1]) {
1188
- this.spreadInRectangle({
1189
- x0: this.gameWidth - this.scalex / 2,
1190
- x1: maxx,
1191
- y0: miny,
1192
- y1: maxy
1193
- });
1194
- } else if (freey - freex > 0.2 * this.scalex || where[2]) {
1195
- this.spreadInRectangle({
1196
- x0: minx,
1197
- x1: maxx,
1198
- y0: this.gameHeight - this.scaley / 2,
1199
- y1: maxy
1200
- });
1201
- } else {
1202
- if (this.gameWidth > this.gameHeight) {
1203
- this.spreadInRectangle({
1204
- x0: minx,
1205
- x1: maxx,
1206
- y0: this.gameHeight - this.scaley / 2,
1207
- y1: maxy
1208
- });
1209
- } else {
1210
- this.spreadInRectangle({
1211
- x0: this.gameWidth - this.scalex / 2,
1212
- x1: maxx,
1213
- y0: miny,
1214
- y1: maxy
1215
- });
1216
- }
1217
- }
1218
- return;
1219
- }
1220
- let nrects = [];
1221
- rects.forEach((rect) => nrects.push(rect));
1222
- let k0 = 0;
1223
- const npTot = this.nx * this.ny;
1224
- for (let k = 0; k < nrects.length; ++k) {
1225
- let k1 = mround(((k + 1) / nrects.length) * npTot);
1226
- this.spreadSetInRectangle(this.polyPieces.slice(k0, k1), nrects[k]);
1227
- k0 = k1;
1228
- }
1229
- arrayShuffle(this.polyPieces);
1230
- this.evaluateZIndex();
1231
- }
1232
-
1233
- evaluateZIndex() {
1234
- for (let k = this.polyPieces.length - 1; k > 0; --k) {
1235
- if (
1236
- this.polyPieces[k].pieces.length > this.polyPieces[k - 1].pieces.length
1237
- ) {
1238
- [this.polyPieces[k], this.polyPieces[k - 1]] = [
1239
- this.polyPieces[k - 1],
1240
- this.polyPieces[k]
1241
- ];
1242
- }
1243
- }
1244
- this.polyPieces.forEach((pp, k) => {
1245
- pp.canvas.style.zIndex = k + 10;
1246
- });
1247
- this.zIndexSup = this.polyPieces.length + 10;
1248
- }
1249
-
1250
- getStateData() {
1251
- let ppData;
1252
- let saved = { signature: FILE_SIGNATURE };
1253
- if ("origin" in this.srcImage.dataset) {
1254
- saved.origin = this.srcImage.dataset.origin;
1255
- }
1256
- saved.src = this.srcImage.src;
1257
- let base = [
1258
- this.nx,
1259
- this.ny,
1260
- this.scalex * this.nx,
1261
- this.prng.seed,
1262
- this.rotationAllowed ? 1 : 0,
1263
- this.typeOfShape,
1264
- this.srcImage.naturalWidth,
1265
- this.srcImage.naturalHeight
1266
- ];
1267
- saved.base = base;
1268
- let pps = [];
1269
- base.push(pps);
1270
- this.polyPieces.forEach((pp) => {
1271
- ppData = [mround(pp.x), mround(pp.y)];
1272
- if (this.rotationAllowed) ppData.push(pp.rot);
1273
- pp.pieces.forEach((p) => ppData.push(p.kx, p.ky));
1274
- pps.push(ppData);
1275
- });
1276
- return saved;
1277
- }
1278
- }
1279
-
1280
- // JigsawPuzzle Wrapper Class
1281
- // This will be appended to jigsaw-puzzle-class.js
1282
-
1283
- // ============================================================================
1284
- // JigsawPuzzle - Main Public API Class
1285
- // ============================================================================
1286
-
1287
- export class JigsawPuzzle {
1288
- /**
1289
- * Creates a new JigsawPuzzle instance
1290
- * @param {string|HTMLElement} containerId - ID of container div or element itself
1291
- * @param {Object} options - Configuration options
1292
- * @param {string} options.image - Image URL or data URL to use
1293
- * @param {number} options.numPieces - Number of puzzle pieces (default: 20)
1294
- * @param {number} options.shapeType - Shape type 0-3 (default: 0)
1295
- * @param {boolean} options.allowRotation - Allow piece rotation (default: false)
1296
- * @param {Function} options.onReady - Callback when puzzle is ready (image loaded, state 15)
1297
- * @param {Function} options.onWin - Callback when puzzle is solved
1298
- * @param {Function} options.onStart - Callback when game starts
1299
- * @param {Function} options.onStop - Callback when game stops
1300
- */
1301
- constructor(containerId, options = {}) {
1302
- const container = typeof containerId === "string"
1303
- ? document.getElementById(containerId)
1304
- : containerId;
1305
-
1306
- if (!container) {
1307
- throw new Error("Container element not found");
1308
- }
1309
-
1310
- // Store options
1311
- this.options = {
1312
- image: options.image || null,
1313
- numPieces: options.numPieces || 20,
1314
- shapeType: options.shapeType || 0,
1315
- allowRotation: options.allowRotation || false,
1316
- onReady: options.onReady || null,
1317
- onWin: options.onWin || null,
1318
- onStart: options.onStart || null,
1319
- onStop: options.onStop || null
1320
- };
1321
-
1322
- // Create internal puzzle instance
1323
- this.puzzle = new InternalPuzzle(container);
1324
- this.puzzle.nbPieces = this.options.numPieces;
1325
- this.puzzle.rotationAllowed = this.options.allowRotation;
1326
- this.puzzle.typeOfShape = this.options.shapeType;
1327
-
1328
- // Animation state
1329
- this.events = [];
1330
- this.state = 0;
1331
- this.moving = {};
1332
- this.tmpImage = null;
1333
- this.lastMousePos = { x: 0, y: 0 };
1334
- this.useMouse = true;
1335
- this.playing = false;
1336
- this.animationFrameId = null;
1337
- this.restoredState = null;
1338
- this.restoredString = "";
1339
-
1340
- // Setup event handlers
1341
- this._setupEventHandlers();
1342
-
1343
- // Setup image load handler
1344
- this.puzzle.srcImage.addEventListener("load", () => this._imageLoaded());
1345
-
1346
- // Setup resize handler
1347
- window.addEventListener("resize", () => {
1348
- if (this.events.length && this.events[this.events.length - 1].event === "resize") return;
1349
- this.events.push({ event: "resize" });
1350
- });
1351
-
1352
- // Load initial image if provided
1353
- if (this.options.image) {
1354
- this.setImage(this.options.image);
1355
- }
1356
-
1357
- // Start animation loop
1358
- this._animate(0);
1359
- }
1360
-
1361
- _setupEventHandlers() {
1362
- const puzzle = this.puzzle;
1363
- const container = puzzle.container;
1364
-
1365
- container.addEventListener("mousedown", (event) => {
1366
- this.useMouse = true;
1367
- event.preventDefault();
1368
- if (event.button !== 0) return;
1369
- this.events.push({
1370
- event: "touch",
1371
- position: puzzle.relativeMouseCoordinates(event)
1372
- });
1373
- });
1374
-
1375
- container.addEventListener("touchstart", (event) => {
1376
- this.useMouse = false;
1377
- event.preventDefault();
1378
- if (event.touches.length === 0) return;
1379
- const rTouch = [];
1380
- for (let k = 0; k < event.touches.length; ++k) {
1381
- rTouch[k] = puzzle.relativeMouseCoordinates(event.touches.item(k));
1382
- }
1383
- if (event.touches.length === 1) {
1384
- this.events.push({ event: "touch", position: rTouch[0] });
1385
- }
1386
- if (event.touches.length === 2) {
1387
- this.events.push({ event: "touches", touches: rTouch });
1388
- }
1389
- }, { passive: false });
1390
-
1391
- const handleLeave = () => {
1392
- this.events.push({ event: "leave" });
1393
- };
1394
-
1395
- container.addEventListener("mouseup", (event) => {
1396
- this.useMouse = true;
1397
- event.preventDefault();
1398
- if (event.button !== 0) return;
1399
- handleLeave();
1400
- });
1401
- container.addEventListener("touchend", handleLeave);
1402
- container.addEventListener("touchleave", handleLeave);
1403
- container.addEventListener("touchcancel", handleLeave);
1404
-
1405
- container.addEventListener("mousemove", (event) => {
1406
- this.useMouse = true;
1407
- event.preventDefault();
1408
- if (this.events.length && this.events[this.events.length - 1].event === "move")
1409
- this.events.pop();
1410
- const pos = puzzle.relativeMouseCoordinates(event);
1411
- this.lastMousePos = pos;
1412
- this.events.push({
1413
- event: "move",
1414
- position: pos,
1415
- ev: event
1416
- });
1417
- });
1418
-
1419
- container.addEventListener("touchmove", (event) => {
1420
- this.useMouse = false;
1421
- event.preventDefault();
1422
- const rTouch = [];
1423
- if (event.touches.length === 0) return;
1424
- for (let k = 0; k < event.touches.length; ++k) {
1425
- rTouch[k] = puzzle.relativeMouseCoordinates(event.touches.item(k));
1426
- }
1427
- if (event.touches.length === 1) {
1428
- if (this.events.length && this.events[this.events.length - 1].event === "move")
1429
- this.events.pop();
1430
- this.events.push({ event: "move", position: rTouch[0] });
1431
- }
1432
- if (event.touches.length === 2) {
1433
- if (this.events.length && this.events[this.events.length - 1].event === "moves")
1434
- this.events.pop();
1435
- this.events.push({ event: "moves", touches: rTouch });
1436
- }
1437
- }, { passive: false });
1438
-
1439
- container.addEventListener("wheel", (event) => {
1440
- this.useMouse = true;
1441
- event.preventDefault();
1442
- if (this.events.length && this.events[this.events.length - 1].event === "wheel")
1443
- this.events.pop();
1444
- this.events.push({ event: "wheel", wheel: event });
1445
- });
1446
- }
1447
-
1448
- _imageLoaded() {
1449
- this.puzzle.imageLoaded = true;
1450
- let event = { event: "srcImageLoaded" };
1451
- if (this.restoredState) {
1452
- if (
1453
- mround(this.puzzle.srcImage.naturalWidth) !== this.restoredState.base[6] ||
1454
- mround(this.puzzle.srcImage.naturalHeight) !== this.restoredState.base[7]
1455
- ) {
1456
- event.event = "wrongImage";
1457
- this.restoredState = null;
1458
- }
1459
- }
1460
- this.events.push(event);
1461
- }
1462
-
1463
- _animate(tStamp) {
1464
- this.animationFrameId = requestAnimationFrame((ts) => this._animate(ts));
1465
-
1466
- let event;
1467
- if (this.events.length) event = this.events.shift();
1468
- if (event && event.event === "reset") this.state = 0;
1469
-
1470
- // Resize event
1471
- if (event && event.event === "resize") {
1472
- const puzzle = this.puzzle;
1473
- const prevWidth = puzzle.contWidth;
1474
- const prevHeight = puzzle.contHeight;
1475
- puzzle.getContainerSize();
1476
- if (this.state === 15 || this.state === 60) {
1477
- fitImage(this.tmpImage, puzzle.contWidth * 0.95, puzzle.contHeight * 0.95);
1478
- } else if (this.state >= 25) {
1479
- const prevGameWidth = puzzle.gameWidth;
1480
- const prevGameHeight = puzzle.gameHeight;
1481
- puzzle.scale();
1482
- const reScale = puzzle.contWidth / prevWidth;
1483
- puzzle.polyPieces.forEach((pp) => {
1484
- let nx = puzzle.contWidth / 2 - (prevWidth / 2 - pp.x) * reScale;
1485
- let ny = puzzle.contHeight / 2 - (prevHeight / 2 - pp.y) * reScale;
1486
- nx = mmin(mmax(nx, -puzzle.scalex / 2), puzzle.contWidth - 1.5 * puzzle.scalex);
1487
- ny = mmin(mmax(ny, -puzzle.scaley / 2), puzzle.contHeight - 1.5 * puzzle.scaley);
1488
- pp.moveTo(nx, ny);
1489
- pp.drawImage();
1490
- });
1491
- }
1492
- return;
1493
- }
1494
-
1495
- switch (this.state) {
1496
- case 0:
1497
- this.state = 10;
1498
- // fall through
1499
-
1500
- case 10:
1501
- this.playing = false;
1502
- if (!this.puzzle.imageLoaded) return;
1503
- this.puzzle.container.innerHTML = "";
1504
- this.tmpImage = document.createElement("img");
1505
- this.tmpImage.src = this.puzzle.srcImage.src;
1506
- this.puzzle.getContainerSize();
1507
- fitImage(this.tmpImage, this.puzzle.contWidth * 0.95, this.puzzle.contHeight * 0.95);
1508
- this.tmpImage.style.boxShadow = "-4px 4px 4px rgba(0, 0, 0, 0.5)";
1509
- this.puzzle.container.appendChild(this.tmpImage);
1510
- this.state = 15;
1511
- // Call onReady callback when puzzle is ready (image loaded and displayed)
1512
- if (this.options.onReady) {
1513
- this.options.onReady();
1514
- }
1515
- break;
1516
-
1517
- case 15:
1518
- this.playing = false;
1519
- if (!event) return;
1520
- if (event.event === "nbpieces") {
1521
- this.puzzle.nbPieces = event.nbpieces;
1522
- this.state = 20;
1523
- } else if (event.event === "srcImageLoaded") {
1524
- this.state = 10;
1525
- return;
1526
- } else if (event.event === "restore") {
1527
- this.state = 150;
1528
- return;
1529
- } else return;
1530
-
1531
- case 20:
1532
- this.playing = true;
1533
- if (this.options.onStart) this.options.onStart();
1534
- this.puzzle.rotationAllowed = this.options.allowRotation;
1535
- if (this.restoredState) {
1536
- this.puzzle.create(this.restoredState.base);
1537
- } else {
1538
- this.puzzle.create();
1539
- }
1540
- if (this.restoredState) {
1541
- this.puzzle.doScale(this.restoredState.base[2]);
1542
- } else {
1543
- this.puzzle.scale();
1544
- }
1545
- this.puzzle.polyPieces.forEach((pp) => {
1546
- pp.drawImage();
1547
- if (this.restoredState) {
1548
- pp.moveTo(pp.x, pp.y);
1549
- } else {
1550
- pp.moveToInitialPlace();
1551
- }
1552
- });
1553
- this.puzzle.gameCanvas.style.top = this.puzzle.offsy + "px";
1554
- this.puzzle.gameCanvas.style.left = this.puzzle.offsx + "px";
1555
- this.puzzle.gameCanvas.style.display = "none";
1556
- this.state = 25;
1557
- if (this.restoredState) {
1558
- this.restoredState = null;
1559
- this.state = 50;
1560
- }
1561
- break;
1562
-
1563
- case 25:
1564
- this.puzzle.gameCanvas.style.display = "none";
1565
- this.puzzle.polyPieces.forEach((pp) => {
1566
- pp.canvas.classList.add("moving");
1567
- });
1568
- this.state = 30;
1569
- break;
1570
-
1571
- case 30:
1572
- this.puzzle.optimInitial();
1573
- setTimeout(() => this.events.push({ event: "finished" }), 1200);
1574
- this.state = 35;
1575
- break;
1576
-
1577
- case 35:
1578
- if (!event || event.event !== "finished") return;
1579
- this.puzzle.polyPieces.forEach((pp) => {
1580
- pp.canvas.classList.remove("moving");
1581
- });
1582
- this.state = 50;
1583
- break;
1584
-
1585
- case 50:
1586
- if (!event) return;
1587
- if (event.event === "stop") {
1588
- this.state = 10;
1589
- return;
1590
- }
1591
- if (event.event === "nbpieces") {
1592
- this.puzzle.nbPieces = event.nbpieces;
1593
- this.state = 20;
1594
- } else if (event.event === "save") {
1595
- this.state = 120;
1596
- } else if (event.event === "touch") {
1597
- this.moving = {
1598
- xMouseInit: event.position.x,
1599
- yMouseInit: event.position.y,
1600
- tInit: tStamp
1601
- };
1602
- for (let k = this.puzzle.polyPieces.length - 1; k >= 0; --k) {
1603
- let pp = this.puzzle.polyPieces[k];
1604
- if (pp.isPointInPath(event.position)) {
1605
- pp.selected = true;
1606
- pp.drawImage();
1607
- this.moving.pp = pp;
1608
- this.moving.ppXInit = pp.x;
1609
- this.moving.ppYInit = pp.y;
1610
- this.puzzle.polyPieces.splice(k, 1);
1611
- this.puzzle.polyPieces.push(pp);
1612
- pp.canvas.style.zIndex = this.puzzle.zIndexSup;
1613
- this.state = 55;
1614
- return;
1615
- }
1616
- }
1617
- this.state = 100;
1618
- } else if (event.event === "touches") {
1619
- this.moving = { touches: event.touches };
1620
- this.state = 110;
1621
- } else if (event.event === "wheel") {
1622
- if (event.wheel.deltaY > 0) this.puzzle.zoomBy(1.3, this.lastMousePos);
1623
- if (event.wheel.deltaY < 0) this.puzzle.zoomBy(1 / 1.3, this.lastMousePos);
1624
- }
1625
- break;
1626
-
1627
- case 55:
1628
- if (!event) return;
1629
- if (event.event === "stop") {
1630
- this.state = 10;
1631
- return;
1632
- }
1633
- switch (event.event) {
1634
- case "moves":
1635
- case "touches":
1636
- this.moving.pp.selected = false;
1637
- this.moving.pp.drawImage();
1638
- this.moving = { touches: event.touches };
1639
- this.state = 110;
1640
- break;
1641
- case "move":
1642
- if (event?.ev?.buttons === 0) {
1643
- this.events.push({ event: "leave" });
1644
- break;
1645
- }
1646
- this.moving.pp.moveTo(
1647
- event.position.x - this.moving.xMouseInit + this.moving.ppXInit,
1648
- event.position.y - this.moving.yMouseInit + this.moving.ppYInit
1649
- );
1650
- break;
1651
- case "leave":
1652
- if (this.puzzle.rotationAllowed && tStamp < this.moving.tInit + 250) {
1653
- this.moving.pp.rotate((this.moving.pp.rot + 1) % 4);
1654
- this.moving.pp.coerceToContainer();
1655
- }
1656
- this.moving.pp.selected = false;
1657
- this.moving.pp.drawImage();
1658
- let merged = false;
1659
- let doneSomething;
1660
- do {
1661
- doneSomething = false;
1662
- for (let k = this.puzzle.polyPieces.length - 1; k >= 0; --k) {
1663
- let pp = this.puzzle.polyPieces[k];
1664
- if (pp === this.moving.pp) continue;
1665
- if (this.moving.pp.ifNear(pp)) {
1666
- merged = true;
1667
- if (pp.pieces.length > this.moving.pp.pieces.length) {
1668
- pp.merge(this.moving.pp);
1669
- this.moving.pp = pp;
1670
- } else {
1671
- this.moving.pp.merge(pp);
1672
- }
1673
- doneSomething = true;
1674
- break;
1675
- }
1676
- }
1677
- } while (doneSomething);
1678
- this.puzzle.evaluateZIndex();
1679
- if (merged) {
1680
- this.moving.pp.selected = true;
1681
- this.moving.pp.drawImage(true);
1682
- this.moving.tInit = tStamp + 500;
1683
- this.state = 56;
1684
- break;
1685
- }
1686
- this.state = 50;
1687
- if (this.puzzle.polyPieces.length === 1 && this.puzzle.polyPieces[0].rot === 0) {
1688
- this.state = 60;
1689
- }
1690
- }
1691
- break;
1692
-
1693
- case 56:
1694
- if (tStamp < this.moving.tInit) return;
1695
- this.moving.pp.selected = false;
1696
- this.moving.pp.drawImage();
1697
- if (this.puzzle.polyPieces.length === 1 && this.puzzle.polyPieces[0].rot === 0)
1698
- this.state = 60;
1699
- else
1700
- this.state = 50;
1701
- break;
1702
-
1703
- case 60:
1704
- this.playing = false;
1705
- if (this.options.onWin) this.options.onWin();
1706
- this.puzzle.container.innerHTML = "";
1707
- this.puzzle.getContainerSize();
1708
- fitImage(this.tmpImage, this.puzzle.contWidth * 0.95, this.puzzle.contHeight * 0.95);
1709
- const finalWidth = this.tmpImage.style.width;
1710
- const finalHeight = this.tmpImage.style.height;
1711
- this.tmpImage.style.width = `${this.puzzle.nx * this.puzzle.scalex}px`;
1712
- this.tmpImage.style.height = `${this.puzzle.ny * this.puzzle.scaley}px`;
1713
- this.tmpImage.style.left = `${((this.puzzle.polyPieces[0].x + this.puzzle.scalex / 2 + this.puzzle.gameWidth / 2) / this.puzzle.contWidth) * 100}%`;
1714
- this.tmpImage.style.top = `${((this.puzzle.polyPieces[0].y + this.puzzle.scaley / 2 + this.puzzle.gameHeight / 2) / this.puzzle.contHeight) * 100}%`;
1715
- this.tmpImage.style.boxShadow = "-4px 4px 4px rgba(0, 0, 0, 0.5)";
1716
- this.tmpImage.classList.add("moving");
1717
- setTimeout(() => {
1718
- this.tmpImage.style.top = this.tmpImage.style.left = "50%";
1719
- this.tmpImage.style.width = finalWidth;
1720
- this.tmpImage.style.height = finalHeight;
1721
- }, 0);
1722
- this.puzzle.container.appendChild(this.tmpImage);
1723
- this.state = 15;
1724
- break;
1725
-
1726
- case 100:
1727
- if (!event) return;
1728
- if (event.event === "move") {
1729
- if (event?.ev?.buttons === 0) {
1730
- this.state = 50;
1731
- break;
1732
- }
1733
- this.puzzle.sweepBy(
1734
- event.position.x - this.moving.xMouseInit,
1735
- event.position.y - this.moving.yMouseInit
1736
- );
1737
- this.moving.xMouseInit = event.position.x;
1738
- this.moving.yMouseInit = event.position.y;
1739
- return;
1740
- }
1741
- if (event.event === "leave") {
1742
- this.state = 50;
1743
- return;
1744
- }
1745
- if (event.event === "touches") {
1746
- this.moving = { touches: event.touches };
1747
- this.state = 110;
1748
- }
1749
- break;
1750
-
1751
- case 110:
1752
- if (!event) return;
1753
- if (event.event === "leave") {
1754
- this.state = 50;
1755
- return;
1756
- }
1757
- if (event.event === "moves") {
1758
- const center = {
1759
- x: (this.moving.touches[0].x + this.moving.touches[1].x) / 2,
1760
- y: (this.moving.touches[0].y + this.moving.touches[1].y) / 2
1761
- };
1762
- const dInit = mhypot(
1763
- this.moving.touches[0].x - this.moving.touches[1].x,
1764
- this.moving.touches[0].y - this.moving.touches[1].y
1765
- );
1766
- const d = mhypot(
1767
- event.touches[0].x - event.touches[1].x,
1768
- event.touches[0].y - event.touches[1].y
1769
- );
1770
- const dRef = msqrt(this.puzzle.contWidth * this.puzzle.contHeight) / 5;
1771
- this.puzzle.zoomBy(Math.exp((d - dInit) / dRef), center);
1772
- this.moving.touches = event.touches;
1773
- return;
1774
- }
1775
- break;
1776
-
1777
- case 120:
1778
- const savedData = this.puzzle.getStateData();
1779
- const savedString = JSON.stringify(savedData);
1780
- if (event && event.callback) {
1781
- event.callback(savedString);
1782
- }
1783
- this.state = 50;
1784
- break;
1785
-
1786
- case 150:
1787
- this.restoredString = "";
1788
- if (event && event.data) {
1789
- this.restoredString = event.data;
1790
- this.state = 155;
1791
- } else {
1792
- try {
1793
- this.restoredString = localStorage.getItem("savepuzzle");
1794
- if (!this.restoredString) this.restoredString = "";
1795
- } catch (exception) {
1796
- this.restoredString = "";
1797
- }
1798
- if (this.restoredString.length === 0) {
1799
- this.state = 15;
1800
- break;
1801
- }
1802
- this.state = 155;
1803
- }
1804
- break;
1805
-
1806
- case 155:
1807
- try {
1808
- this.restoredState = JSON.parse(this.restoredString);
1809
- } catch (error) {
1810
- this.restoredState = null;
1811
- this.state = 10;
1812
- break;
1813
- }
1814
- if (
1815
- !this.restoredState.signature ||
1816
- this.restoredState.signature !== FILE_SIGNATURE ||
1817
- !this.restoredState.src
1818
- ) {
1819
- this.restoredState = null;
1820
- this.state = 10;
1821
- break;
1822
- }
1823
- this.puzzle.imageLoaded = false;
1824
- this.puzzle.srcImage.src = this.restoredState.src;
1825
- if (this.restoredState.origin)
1826
- this.puzzle.srcImage.dataset.origin = this.restoredState.origin;
1827
- else
1828
- delete this.puzzle.srcImage.dataset.origin;
1829
- this.state = 158;
1830
- // fall through
1831
-
1832
- case 158:
1833
- if (event && event.event === "srcImageLoaded") {
1834
- this.state = 160;
1835
- } else if (event && event.event === "wrongImage") {
1836
- this.state = 10;
1837
- }
1838
- break;
1839
-
1840
- case 160:
1841
- this.tmpImage.src = this.puzzle.srcImage.src;
1842
- fitImage(this.tmpImage, this.puzzle.contWidth * 0.95, this.puzzle.contHeight * 0.95);
1843
- this.state = 20;
1844
- break;
1845
- }
1846
- }
1847
-
1848
- // ============================================================================
1849
- // Public API Methods
1850
- // ============================================================================
1851
-
1852
- /**
1853
- * Start a new game with the current settings
1854
- */
1855
- start() {
1856
- this.events.push({ event: "nbpieces", nbpieces: this.options.numPieces });
1857
- }
1858
-
1859
- /**
1860
- * Stop the current game
1861
- */
1862
- stop() {
1863
- this.playing = false;
1864
- if (this.options.onStop) this.options.onStop();
1865
- this.events.push({ event: "stop" });
1866
- }
1867
-
1868
- /**
1869
- * Reset the puzzle to initial state
1870
- * Use this to start a new game with the same instance and container
1871
- * The puzzle will reload the current image and be ready for a new game
1872
- */
1873
- reset() {
1874
- this.events.push({ event: "reset" });
1875
- this.state = 0;
1876
- this.playing = false;
1877
- this.restoredState = null;
1878
- this.restoredString = "";
1879
- }
1880
-
1881
- /**
1882
- * Save the current game state
1883
- * @param {Function} callback - Optional callback to receive saved data as JSON string
1884
- */
1885
- save(callback) {
1886
- if (callback) {
1887
- this.events.push({ event: "save", callback });
1888
- } else {
1889
- // Default: save to localStorage
1890
- this.events.push({ event: "save", callback: (data) => {
1891
- try {
1892
- localStorage.setItem("savepuzzle", data);
1893
- } catch (exception) {
1894
- console.error("Failed to save to localStorage:", exception);
1895
- }
1896
- }});
1897
- }
1898
- }
1899
-
1900
- /**
1901
- * Load a saved game state
1902
- * @param {string} savedData - JSON string of saved data, or null to load from localStorage
1903
- */
1904
- load(savedData) {
1905
- this.events.push({ event: "restore", data: savedData });
1906
- }
1907
-
1908
- /**
1909
- * Set the puzzle image
1910
- * @param {string} imageUrl - URL or data URL of the image
1911
- */
1912
- setImage(imageUrl) {
1913
- this.options.image = imageUrl;
1914
- this.puzzle.imageLoaded = false;
1915
- this.puzzle.srcImage.src = imageUrl;
1916
- delete this.puzzle.srcImage.dataset.origin;
1917
- }
1918
-
1919
- /**
1920
- * Update puzzle options
1921
- * @param {Object} newOptions - Options to update
1922
- */
1923
- setOptions(newOptions) {
1924
- Object.assign(this.options, newOptions);
1925
- if (newOptions.numPieces !== undefined) {
1926
- this.puzzle.nbPieces = newOptions.numPieces;
1927
- }
1928
- if (newOptions.allowRotation !== undefined) {
1929
- this.puzzle.rotationAllowed = newOptions.allowRotation;
1930
- }
1931
- if (newOptions.shapeType !== undefined) {
1932
- this.puzzle.typeOfShape = newOptions.shapeType;
1933
- }
1934
- }
1935
-
1936
- /**
1937
- * Destroy the puzzle instance and clean up completely
1938
- * Use this when you want to remove the puzzle entirely and create a new one
1939
- * For reusing the same instance with a new game, use reset() instead
1940
- */
1941
- destroy() {
1942
- // Stop animation loop
1943
- if (this.animationFrameId) {
1944
- cancelAnimationFrame(this.animationFrameId);
1945
- this.animationFrameId = null;
1946
- }
1947
-
1948
- // Clear events
1949
- this.events = [];
1950
- this.playing = false;
1951
-
1952
- // Clear container (removes all puzzle elements)
1953
- if (this.puzzle && this.puzzle.container) {
1954
- this.puzzle.container.innerHTML = "";
1955
- }
1956
-
1957
- // Note: Event listeners are not removed because they're bound to the container
1958
- // If you need to completely remove listeners, you'd need to store references
1959
- // For most use cases, clearing innerHTML and stopping animation is sufficient
1960
- }
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
+ this.rotationDegrees = 0; // Cumulative rotation in degrees for smooth animation
398
+ }
399
+
400
+ merge(otherPoly) {
401
+ const puzzle = this.puzzle;
402
+ const orgpckxmin = this.pckxmin;
403
+ const orgpckymin = this.pckymin;
404
+ const pbefore = getTransformed(
405
+ 0,
406
+ 0,
407
+ this.nx * puzzle.scalex,
408
+ this.ny * puzzle.scaley,
409
+ this.rot
410
+ );
411
+
412
+ const kOther = puzzle.polyPieces.indexOf(otherPoly);
413
+ puzzle.polyPieces.splice(kOther, 1);
414
+ puzzle.container.removeChild(otherPoly.canvas);
415
+
416
+ for (let k = 0; k < otherPoly.pieces.length; ++k) {
417
+ this.pieces.push(otherPoly.pieces[k]);
418
+ if (otherPoly.pieces[k].kx < this.pckxmin)
419
+ this.pckxmin = otherPoly.pieces[k].kx;
420
+ if (otherPoly.pieces[k].kx + 1 > this.pckxmax)
421
+ this.pckxmax = otherPoly.pieces[k].kx + 1;
422
+ if (otherPoly.pieces[k].ky < this.pckymin)
423
+ this.pckymin = otherPoly.pieces[k].ky;
424
+ if (otherPoly.pieces[k].ky + 1 > this.pckymax)
425
+ this.pckymax = otherPoly.pieces[k].ky + 1;
426
+ }
427
+
428
+ this.pieces.sort(function (p1, p2) {
429
+ if (p1.ky < p2.ky) return -1;
430
+ if (p1.ky > p2.ky) return 1;
431
+ if (p1.kx < p2.kx) return -1;
432
+ if (p1.kx > p2.kx) return 1;
433
+ return 0;
434
+ });
435
+
436
+ this.listLoops();
437
+ this.drawImage();
438
+
439
+ const pafter = getTransformed(
440
+ puzzle.scalex * (orgpckxmin - this.pckxmin),
441
+ puzzle.scaley * (orgpckymin - this.pckymin),
442
+ puzzle.scalex * (this.pckxmax - this.pckxmin + 1),
443
+ puzzle.scaley * (this.pckymax - this.pckymin + 1),
444
+ this.rot
445
+ );
446
+
447
+ this.moveTo(this.x - pafter.x + pbefore.x, this.y - pafter.y + pbefore.y);
448
+ puzzle.evaluateZIndex();
449
+
450
+ function getTransformed(orgx, orgy, width, height, rot) {
451
+ const dx = orgx - width / 2;
452
+ const dy = orgy - height / 2;
453
+ return {
454
+ x: width / 2 + [1, 0, -1, 0][rot] * dx + [0, -1, 0, 1][rot] * dy,
455
+ y: height / 2 + [0, 1, 0, -1][rot] * dx + [1, 0, -1, 0][rot] * dy
456
+ };
457
+ }
458
+ }
459
+
460
+ ifNear(otherPoly) {
461
+ const puzzle = this.puzzle;
462
+ if (this.rot != otherPoly.rot) return false;
463
+
464
+ let p1, p2;
465
+ let org = this.getOrgP();
466
+ let orgOther = otherPoly.getOrgP();
467
+
468
+ if (mhypot(org.x - orgOther.x, org.y - orgOther.y) >= puzzle.dConnect)
469
+ return false;
470
+
471
+ for (let k = this.pieces.length - 1; k >= 0; --k) {
472
+ p1 = this.pieces[k];
473
+ for (let ko = otherPoly.pieces.length - 1; ko >= 0; --ko) {
474
+ p2 = otherPoly.pieces[ko];
475
+ if (p1.kx == p2.kx && mabs(p1.ky - p2.ky) == 1) return true;
476
+ if (p1.ky == p2.ky && mabs(p1.kx - p2.kx) == 1) return true;
477
+ }
478
+ }
479
+
480
+ return false;
481
+ }
482
+
483
+ listLoops() {
484
+ const that = this;
485
+ function edgeIsCommon(kx, ky, edge) {
486
+ let k;
487
+ switch (edge) {
488
+ case 0: ky--; break;
489
+ case 1: kx++; break;
490
+ case 2: ky++; break;
491
+ case 3: kx--; break;
492
+ }
493
+ for (k = 0; k < that.pieces.length; k++) {
494
+ if (kx == that.pieces[k].kx && ky == that.pieces[k].ky) return true;
495
+ }
496
+ return false;
497
+ }
498
+
499
+ function edgeIsInTbEdges(kx, ky, edge) {
500
+ let k;
501
+ for (k = 0; k < tbEdges.length; k++) {
502
+ if (
503
+ kx == tbEdges[k].kx &&
504
+ ky == tbEdges[k].ky &&
505
+ edge == tbEdges[k].edge
506
+ )
507
+ return k;
508
+ }
509
+ return false;
510
+ }
511
+
512
+ let tbLoops = [];
513
+ let tbEdges = [];
514
+ let k;
515
+ let kEdge;
516
+ let lp;
517
+ let currEdge;
518
+ let tries;
519
+ let edgeNumber;
520
+ let potNext;
521
+
522
+ let tbTries = [
523
+ [
524
+ { dkx: 0, dky: 0, edge: 1 },
525
+ { dkx: 1, dky: 0, edge: 0 },
526
+ { dkx: 1, dky: -1, edge: 3 }
527
+ ],
528
+ [
529
+ { dkx: 0, dky: 0, edge: 2 },
530
+ { dkx: 0, dky: 1, edge: 1 },
531
+ { dkx: 1, dky: 1, edge: 0 }
532
+ ],
533
+ [
534
+ { dkx: 0, dky: 0, edge: 3 },
535
+ { dkx: -1, dky: 0, edge: 2 },
536
+ { dkx: -1, dky: 1, edge: 1 }
537
+ ],
538
+ [
539
+ { dkx: 0, dky: 0, edge: 0 },
540
+ { dkx: 0, dky: -1, edge: 3 },
541
+ { dkx: -1, dky: -1, edge: 2 }
542
+ ]
543
+ ];
544
+
545
+ for (k = 0; k < this.pieces.length; k++) {
546
+ for (kEdge = 0; kEdge < 4; kEdge++) {
547
+ if (!edgeIsCommon(this.pieces[k].kx, this.pieces[k].ky, kEdge))
548
+ tbEdges.push({
549
+ kx: this.pieces[k].kx,
550
+ ky: this.pieces[k].ky,
551
+ edge: kEdge,
552
+ kp: k
553
+ });
554
+ }
555
+ }
556
+
557
+ while (tbEdges.length > 0) {
558
+ lp = [];
559
+ currEdge = tbEdges[0];
560
+ lp.push(currEdge);
561
+ tbEdges.splice(0, 1);
562
+ do {
563
+ for (tries = 0; tries < 3; tries++) {
564
+ potNext = tbTries[currEdge.edge][tries];
565
+ edgeNumber = edgeIsInTbEdges(
566
+ currEdge.kx + potNext.dkx,
567
+ currEdge.ky + potNext.dky,
568
+ potNext.edge
569
+ );
570
+ if (edgeNumber === false) continue;
571
+ currEdge = tbEdges[edgeNumber];
572
+ lp.push(currEdge);
573
+ tbEdges.splice(edgeNumber, 1);
574
+ break;
575
+ }
576
+ if (edgeNumber === false) break;
577
+ } while (1);
578
+ tbLoops.push(lp);
579
+ }
580
+
581
+ this.tbLoops = tbLoops.map((loop) =>
582
+ loop.map((edge) => {
583
+ let cell = this.pieces[edge.kp];
584
+ if (edge.edge == 0) return cell.ts;
585
+ if (edge.edge == 1) return cell.rs;
586
+ if (edge.edge == 2) return cell.bs;
587
+ return cell.ls;
588
+ })
589
+ );
590
+ }
591
+
592
+ getRect() {
593
+ const puzzle = this.puzzle;
594
+ let rect0 = puzzle.container.getBoundingClientRect();
595
+ let rect = this.canvas.getBoundingClientRect();
596
+ return {
597
+ x: rect.x - rect0.x,
598
+ y: rect.y - rect0.y,
599
+ right: rect.right - rect0.x,
600
+ bottom: rect.bottom - rect0.y,
601
+ width: rect.width,
602
+ height: rect.height
603
+ };
604
+ }
605
+
606
+ getOrgP() {
607
+ const puzzle = this.puzzle;
608
+ const rect = this.getRect();
609
+ switch (this.rot) {
610
+ case 0:
611
+ return {
612
+ x: rect.x - puzzle.scalex * this.pckxmin,
613
+ y: rect.y - puzzle.scaley * this.pckymin
614
+ };
615
+ case 1:
616
+ return {
617
+ x: rect.right + puzzle.scaley * this.pckymin,
618
+ y: rect.y - puzzle.scalex * this.pckxmin
619
+ };
620
+ case 2:
621
+ return {
622
+ x: rect.right + puzzle.scalex * this.pckxmin,
623
+ y: rect.bottom + puzzle.scaley * this.pckymin
624
+ };
625
+ case 3:
626
+ return {
627
+ x: rect.x - puzzle.scaley * this.pckymin,
628
+ y: rect.bottom + puzzle.scalex * this.pckxmin
629
+ };
630
+ }
631
+ }
632
+
633
+ drawPath(ctx, shiftx, shifty) {
634
+ this.tbLoops.forEach((loop) => {
635
+ let without = false;
636
+ loop.forEach((side) => {
637
+ side.drawPath(ctx, shiftx, shifty, without);
638
+ without = true;
639
+ });
640
+ ctx.closePath();
641
+ });
642
+ }
643
+
644
+ drawImage(special) {
645
+ const puzzle = this.puzzle;
646
+ this.nx = this.pckxmax - this.pckxmin + 1;
647
+ this.ny = this.pckymax - this.pckymin + 1;
648
+ this.canvas.width = this.nx * puzzle.scalex;
649
+ this.canvas.height = this.ny * puzzle.scaley;
650
+
651
+ this.offsx = (this.pckxmin - 0.5) * puzzle.scalex;
652
+ this.offsy = (this.pckymin - 0.5) * puzzle.scaley;
653
+
654
+ this.path = new Path2D();
655
+ this.drawPath(this.path, -this.offsx, -this.offsy);
656
+
657
+ this.ctx.fillStyle = "none";
658
+ this.ctx.shadowColor = this.selected
659
+ ? special
660
+ ? "lime"
661
+ : "gold"
662
+ : "rgba(0, 0, 0, 0.5)";
663
+ this.ctx.shadowBlur = this.selected ? mmin(8, puzzle.scalex / 10) : 4;
664
+ this.ctx.shadowOffsetX = this.selected ? 0 : -4;
665
+ this.ctx.shadowOffsetY = this.selected ? 0 : 4;
666
+ this.ctx.fill(this.path);
667
+ if (this.selected) {
668
+ for (let i = 0; i < 6; i++) this.ctx.fill(this.path);
669
+ }
670
+ this.ctx.shadowColor = "rgba(0, 0, 0, 0)";
671
+
672
+ this.pieces.forEach((pp) => {
673
+ this.ctx.save();
674
+
675
+ const path = new Path2D();
676
+ const shiftx = -this.offsx;
677
+ const shifty = -this.offsy;
678
+ pp.ts.drawPath(path, shiftx, shifty, false);
679
+ pp.rs.drawPath(path, shiftx, shifty, true);
680
+ pp.bs.drawPath(path, shiftx, shifty, true);
681
+ pp.ls.drawPath(path, shiftx, shifty, true);
682
+ path.closePath();
683
+
684
+ this.ctx.clip(path);
685
+ const srcx = pp.kx ? (pp.kx - 0.5) * puzzle.scalex : 0;
686
+ const srcy = pp.ky ? (pp.ky - 0.5) * puzzle.scaley : 0;
687
+
688
+ const destx =
689
+ (pp.kx ? 0 : puzzle.scalex / 2) +
690
+ (pp.kx - this.pckxmin) * puzzle.scalex;
691
+ const desty =
692
+ (pp.ky ? 0 : puzzle.scaley / 2) +
693
+ (pp.ky - this.pckymin) * puzzle.scaley;
694
+
695
+ let w = 2 * puzzle.scalex;
696
+ let h = 2 * puzzle.scaley;
697
+ if (srcx + w > puzzle.gameCanvas.width)
698
+ w = puzzle.gameCanvas.width - srcx;
699
+ if (srcy + h > puzzle.gameCanvas.height)
700
+ h = puzzle.gameCanvas.height - srcy;
701
+
702
+ this.ctx.drawImage(
703
+ puzzle.gameCanvas,
704
+ srcx,
705
+ srcy,
706
+ w,
707
+ h,
708
+ destx,
709
+ desty,
710
+ w,
711
+ h
712
+ );
713
+ this.ctx.lineWidth = puzzle.embossThickness * 1.5;
714
+
715
+ this.ctx.translate(
716
+ puzzle.embossThickness / 2,
717
+ -puzzle.embossThickness / 2
718
+ );
719
+ this.ctx.strokeStyle = "rgba(0, 0, 0, 0.35)";
720
+ this.ctx.stroke(path);
721
+
722
+ this.ctx.translate(-puzzle.embossThickness, puzzle.embossThickness);
723
+ this.ctx.strokeStyle = "rgba(255, 255, 255, 0.35)";
724
+ this.ctx.stroke(path);
725
+
726
+ this.ctx.restore();
727
+ this.canvas.style.transform = `rotate(${this.rotationDegrees}deg)`;
728
+ });
729
+ }
730
+
731
+ moveTo(x, y) {
732
+ this.x = x;
733
+ this.y = y;
734
+ this.canvas.style.left = x + "px";
735
+ this.canvas.style.top = y + "px";
736
+ }
737
+
738
+ moveToInitialPlace() {
739
+ const puzzle = this.puzzle;
740
+ this.moveTo(
741
+ puzzle.offsx + (this.pckxmin - 0.5) * puzzle.scalex,
742
+ puzzle.offsy + (this.pckymin - 0.5) * puzzle.scaley
743
+ );
744
+ }
745
+
746
+ rotate(angle) {
747
+ // Calculate the change in rotation
748
+ const oldRot = this.rot;
749
+ this.rot = angle;
750
+
751
+ // Always rotate forward (add 90 degrees for each increment)
752
+ // If we're wrapping from 3 to 0, we need to go 270->360 instead of 270->0
753
+ if (this.rot === 0 && oldRot === 3) {
754
+ // Going from 270° (rot=3) to 0° (rot=0), add 90° to go to 360°
755
+ this.rotationDegrees += 90;
756
+ } else {
757
+ // Normal case: calculate the difference and add it
758
+ const diff = this.rot - oldRot;
759
+ this.rotationDegrees += diff * 90;
760
+ }
761
+ }
762
+
763
+ isPointInPath(p) {
764
+ let npath = new Path2D();
765
+ this.drawPath(npath, 0, 0);
766
+ let rect = this.getRect();
767
+
768
+ let pRefx = [rect.x, rect.right, rect.right, rect.x][this.rot];
769
+ let pRefy = [rect.y, rect.y, rect.bottom, rect.bottom][this.rot];
770
+
771
+ let mposx =
772
+ [1, 0, -1, 0][this.rot] * (p.x - pRefx) +
773
+ [0, 1, 0, -1][this.rot] * (p.y - pRefy);
774
+ let mposy =
775
+ [0, -1, 0, 1][this.rot] * (p.x - pRefx) +
776
+ [1, 0, -1, 0][this.rot] * (p.y - pRefy);
777
+
778
+ return this.ctx.isPointInPath(this.path, mposx, mposy);
779
+ }
780
+
781
+ coerceToContainer() {
782
+ const puzzle = this.puzzle;
783
+ let dimx = [puzzle.scalex, puzzle.scaley, puzzle.scalex, puzzle.scaley][
784
+ this.rot
785
+ ];
786
+ let dimy = [puzzle.scaley, puzzle.scalex, puzzle.scaley, puzzle.scalex][
787
+ this.rot
788
+ ];
789
+ const rect = this.getRect();
790
+ if (rect.y > -dimy && rect.bottom < puzzle.contHeight + dimy) {
791
+ if (rect.right < dimx) {
792
+ this.moveTo(this.x + dimx - rect.right, this.y);
793
+ return;
794
+ }
795
+ if (rect.x > puzzle.contWidth - dimx) {
796
+ this.moveTo(this.x + puzzle.contWidth - dimx - rect.x, this.y);
797
+ return;
798
+ }
799
+ return;
800
+ }
801
+ if (rect.x > -dimx && rect.right < puzzle.contHeight + dimy) {
802
+ if (rect.bottom < dimy) {
803
+ this.moveTo(this.x, this.y + dimy - rect.bottom);
804
+ return;
805
+ }
806
+ if (rect.y > puzzle.contHeight - dimy) {
807
+ this.moveTo(this.x, this.y + puzzle.contHeight - dimy - rect.y);
808
+ return;
809
+ }
810
+ return;
811
+ }
812
+ if (rect.y < -dimy) {
813
+ this.moveTo(this.x, this.y - rect.y - dimy);
814
+ this.getRect();
815
+ }
816
+ if (rect.bottom > puzzle.contHeight + dimy) {
817
+ this.moveTo(this.x, this.y + puzzle.contHeight + dimy - rect.bottom);
818
+ this.getRect();
819
+ }
820
+ if (rect.right < dimx) {
821
+ this.moveTo(this.x + dimx - rect.right, this.y);
822
+ return;
823
+ }
824
+ if (rect.x > puzzle.contWidth - dimx) {
825
+ this.moveTo(this.x + puzzle.contWidth - dimx - rect.x, this.y);
826
+ return;
827
+ }
828
+ }
829
+ }
830
+
831
+ // ============================================================================
832
+ // Helper Functions
833
+ // ============================================================================
834
+
835
+ function fitImage(img, width, height) {
836
+ let wn = img.naturalWidth;
837
+ let hn = img.naturalHeight;
838
+ let w = width;
839
+ let h = (w * hn) / wn;
840
+ if (h > height) {
841
+ h = height;
842
+ w = (h * wn) / hn;
843
+ }
844
+ img.style.position = "absolute";
845
+ img.style.width = w + "px";
846
+ img.style.height = h + "px";
847
+ img.style.top = "50%";
848
+ img.style.left = "50%";
849
+ img.style.transform = "translate(-50%,-50%)";
850
+ }
851
+
852
+ // ============================================================================
853
+ // Internal Puzzle Class (refactored to be UI-independent)
854
+ // ============================================================================
855
+
856
+ class InternalPuzzle {
857
+ constructor(container) {
858
+ this.container = typeof container === "string"
859
+ ? document.getElementById(container)
860
+ : container;
861
+
862
+ this.gameCanvas = document.createElement("CANVAS");
863
+ this.container.appendChild(this.gameCanvas);
864
+
865
+ this.srcImage = new Image();
866
+ this.imageLoaded = false;
867
+
868
+ // State
869
+ this.nbPieces = 20;
870
+ this.rotationAllowed = false;
871
+ this.typeOfShape = 0;
872
+ }
873
+
874
+ getContainerSize() {
875
+ let styl = window.getComputedStyle(this.container);
876
+ this.contWidth = parseFloat(styl.width);
877
+ this.contHeight = parseFloat(styl.height);
878
+ }
879
+
880
+ create(baseData) {
881
+ this.prng = mMash(baseData ? baseData[3] : null);
882
+ this.container.innerHTML = "";
883
+ this.getContainerSize();
884
+
885
+ if (baseData) {
886
+ this.nx = baseData[0];
887
+ this.ny = baseData[1];
888
+ // baseData[2] is total game width (scalex * nx), not scalex itself
889
+ // scalex will be calculated in doScale()
890
+ this.rotationAllowed = !!baseData[4];
891
+ this.typeOfShape = baseData[5];
892
+ } else {
893
+ this.computenxAndny();
894
+ }
895
+
896
+ this.relativeHeight =
897
+ this.srcImage.naturalHeight /
898
+ this.ny /
899
+ (this.srcImage.naturalWidth / this.nx);
900
+
901
+ if (!baseData) {
902
+ this.typeOfShape = this.typeOfShape || 0;
903
+ }
904
+
905
+ this.defineShapes({
906
+ coeffDecentr: 0.12,
907
+ twistf: [twist0, twist1, twist2, twist3][this.typeOfShape]
908
+ });
909
+
910
+ this.polyPieces = [];
911
+ if (!baseData) {
912
+ this.pieces.forEach((row) =>
913
+ row.forEach((piece) => {
914
+ this.polyPieces.push(new PolyPiece(piece, this));
915
+ })
916
+ );
917
+ arrayShuffle(this.polyPieces);
918
+ if (this.rotationAllowed)
919
+ this.polyPieces.forEach((pp) => {
920
+ pp.rot = intAlea(4);
921
+ pp.rotationDegrees = pp.rot * 90;
922
+ });
923
+ } else {
924
+ const pps = baseData[8];
925
+ const offs = this.rotationAllowed ? 3 : 2;
926
+ pps.forEach((ppData) => {
927
+ let polyp = new PolyPiece(this.pieces[ppData[offs + 1]][ppData[offs]], this);
928
+ polyp.x = ppData[0];
929
+ polyp.y = ppData[1];
930
+ polyp.rot = this.rotationAllowed ? ppData[2] : 0;
931
+ polyp.rotationDegrees = polyp.rot * 90;
932
+ for (let k = offs + 2; k < ppData.length; k += 2) {
933
+ let kx = ppData[k];
934
+ let ky = ppData[k + 1];
935
+ polyp.pieces.push(this.pieces[ky][kx]);
936
+ polyp.pckxmin = mmin(polyp.pckxmin, kx);
937
+ polyp.pckxmax = mmax(polyp.pckxmax, kx + 1);
938
+ polyp.pckymin = mmin(polyp.pckymin, ky);
939
+ polyp.pckymax = mmax(polyp.pckymax, ky + 1);
940
+ }
941
+ polyp.listLoops();
942
+ this.polyPieces.push(polyp);
943
+ });
944
+ }
945
+ this.evaluateZIndex();
946
+ }
947
+
948
+ computenxAndny() {
949
+ let kx, ky,
950
+ width = this.srcImage.naturalWidth,
951
+ height = this.srcImage.naturalHeight,
952
+ npieces = this.nbPieces;
953
+ let err, errmin = 1e9;
954
+ let ncv, nch;
955
+
956
+ let nHPieces = mround(msqrt((npieces * width) / height));
957
+ let nVPieces = mround(npieces / nHPieces);
958
+
959
+ for (ky = 0; ky < 5; ky++) {
960
+ ncv = nVPieces + ky - 2;
961
+ for (kx = 0; kx < 5; kx++) {
962
+ nch = nHPieces + kx - 2;
963
+ err = (nch * height) / ncv / width;
964
+ err = err + 1 / err - 2;
965
+ err += mabs(1 - (nch * ncv) / npieces);
966
+
967
+ if (err < errmin) {
968
+ errmin = err;
969
+ this.nx = nch;
970
+ this.ny = ncv;
971
+ }
972
+ }
973
+ }
974
+ }
975
+
976
+ defineShapes(shapeDesc) {
977
+ let { coeffDecentr, twistf } = shapeDesc;
978
+ const corners = [];
979
+ const nx = this.nx, ny = this.ny;
980
+ let np;
981
+
982
+ for (let ky = 0; ky <= ny; ++ky) {
983
+ corners[ky] = [];
984
+ for (let kx = 0; kx <= nx; ++kx) {
985
+ corners[ky][kx] = new Point(
986
+ kx + this.prng.alea(-coeffDecentr, coeffDecentr),
987
+ ky + this.prng.alea(-coeffDecentr, coeffDecentr)
988
+ );
989
+ if (kx == 0) corners[ky][kx].x = 0;
990
+ if (kx == nx) corners[ky][kx].x = nx;
991
+ if (ky == 0) corners[ky][kx].y = 0;
992
+ if (ky == ny) corners[ky][kx].y = ny;
993
+ }
994
+ }
995
+
996
+ this.pieces = [];
997
+ for (let ky = 0; ky < ny; ++ky) {
998
+ this.pieces[ky] = [];
999
+ for (let kx = 0; kx < nx; ++kx) {
1000
+ this.pieces[ky][kx] = np = new Piece(kx, ky);
1001
+ if (ky == 0) {
1002
+ np.ts.points = [corners[ky][kx], corners[ky][kx + 1]];
1003
+ np.ts.type = "d";
1004
+ } else {
1005
+ np.ts = this.pieces[ky - 1][kx].bs.reversed();
1006
+ }
1007
+ np.rs.points = [corners[ky][kx + 1], corners[ky + 1][kx + 1]];
1008
+ np.rs.type = "d";
1009
+ if (kx < nx - 1) {
1010
+ if (this.prng.intAlea(2))
1011
+ twistf(np.rs, corners[ky][kx], corners[ky + 1][kx], this.prng);
1012
+ else twistf(np.rs, corners[ky][kx + 2], corners[ky + 1][kx + 2], this.prng);
1013
+ }
1014
+ if (kx == 0) {
1015
+ np.ls.points = [corners[ky + 1][kx], corners[ky][kx]];
1016
+ np.ls.type = "d";
1017
+ } else {
1018
+ np.ls = this.pieces[ky][kx - 1].rs.reversed();
1019
+ }
1020
+ np.bs.points = [corners[ky + 1][kx + 1], corners[ky + 1][kx]];
1021
+ np.bs.type = "d";
1022
+ if (ky < ny - 1) {
1023
+ if (this.prng.intAlea(2))
1024
+ twistf(np.bs, corners[ky][kx + 1], corners[ky][kx], this.prng);
1025
+ else twistf(np.bs, corners[ky + 2][kx + 1], corners[ky + 2][kx], this.prng);
1026
+ }
1027
+ }
1028
+ }
1029
+ }
1030
+
1031
+ scale() {
1032
+ const maxWidth = 0.95 * this.contWidth;
1033
+ const maxHeight = 0.95 * this.contHeight;
1034
+ const woh = this.srcImage.naturalWidth / this.srcImage.naturalHeight;
1035
+ let bestWidth = 0;
1036
+ let piecex, piecey;
1037
+
1038
+ let xtra = this.nx * this.ny * 1.2;
1039
+ for (let extrax = 0; extrax <= mceil(this.nx * 0.2); ++extrax) {
1040
+ let availx = extrax == 0 ? maxWidth : this.contWidth;
1041
+ for (let extray = 0; extray <= mceil(this.ny * 0.2); ++extray) {
1042
+ if ((this.nx + extrax) * (this.ny + extray) < xtra) continue;
1043
+ let availy = extray == 0 ? maxHeight : this.contHeight;
1044
+ piecex = availx / (this.nx + extrax);
1045
+ piecey = (piecex * this.nx) / woh / this.ny;
1046
+ if (piecey * (this.ny + extray) > availy) {
1047
+ piecey = availy / (this.ny + extray);
1048
+ piecex = (piecey * this.ny * woh) / this.nx;
1049
+ }
1050
+ if (piecex * this.nx > bestWidth) bestWidth = piecex * this.nx;
1051
+ }
1052
+ }
1053
+
1054
+ this.doScale(bestWidth);
1055
+ }
1056
+
1057
+ doScale(width) {
1058
+ this.gameWidth = width;
1059
+ this.gameHeight = (width * this.srcImage.naturalHeight) / this.srcImage.naturalWidth;
1060
+
1061
+ this.gameCanvas.width = this.gameWidth;
1062
+ this.gameCanvas.height = this.gameHeight;
1063
+ this.gameCtx = this.gameCanvas.getContext("2d");
1064
+ this.gameCtx.drawImage(this.srcImage, 0, 0, this.gameWidth, this.gameHeight);
1065
+
1066
+ this.gameCanvas.classList.add("gameCanvas");
1067
+ this.gameCanvas.style.zIndex = 500;
1068
+
1069
+ this.scalex = this.gameWidth / this.nx;
1070
+ this.scaley = this.gameHeight / this.ny;
1071
+
1072
+ this.pieces.forEach((row) => {
1073
+ row.forEach((piece) => piece.scale(this));
1074
+ });
1075
+
1076
+ this.offsx = (this.contWidth - this.gameWidth) / 2;
1077
+ this.offsy = (this.contHeight - this.gameHeight) / 2;
1078
+ this.dConnect = mmax(10, mmin(this.scalex, this.scaley) / 10);
1079
+ this.embossThickness = mmin(2 + (this.scalex / 200) * (5 - 2), 5);
1080
+ }
1081
+
1082
+ sweepBy(dx, dy) {
1083
+ this.polyPieces.forEach((pp) => {
1084
+ pp.moveTo(pp.x + dx, pp.y + dy);
1085
+ });
1086
+ }
1087
+
1088
+ zoomBy(coef, center) {
1089
+ let futWidth = this.gameWidth * coef;
1090
+ let futHeight = this.gameHeight * coef;
1091
+ if (
1092
+ ((futWidth > 3000 || futHeight > 3000) && coef > 1) ||
1093
+ (futWidth < 200 || futHeight < 200) & (coef < 1)
1094
+ )
1095
+ return;
1096
+ if (coef == 1) return;
1097
+
1098
+ this.doScale(futWidth);
1099
+ this.polyPieces.forEach((pp) => {
1100
+ pp.moveTo(
1101
+ coef * (pp.x - center.x) + center.x,
1102
+ coef * (pp.y - center.y) + center.y
1103
+ );
1104
+ pp.drawImage();
1105
+ });
1106
+ }
1107
+
1108
+ relativeMouseCoordinates(event) {
1109
+ const br = this.container.getBoundingClientRect();
1110
+ return {
1111
+ x: event.clientX - br.x,
1112
+ y: event.clientY - br.y
1113
+ };
1114
+ }
1115
+
1116
+ limitRectangle(rect) {
1117
+ let minscale = mmin(this.scalex, this.scaley);
1118
+ rect.x0 = mmin(mmax(rect.x0, -minscale / 2), this.contWidth - 1.5 * minscale);
1119
+ rect.x1 = mmin(mmax(rect.x1, -minscale / 2), this.contWidth - 1.5 * minscale);
1120
+ rect.y0 = mmin(mmax(rect.y0, -minscale / 2), this.contHeight - 1.5 * minscale);
1121
+ rect.y1 = mmin(mmax(rect.y1, -minscale / 2), this.contHeight - 1.5 * minscale);
1122
+ }
1123
+
1124
+ spreadInRectangle(rect) {
1125
+ this.limitRectangle(rect);
1126
+ this.polyPieces.forEach((pp) =>
1127
+ pp.moveTo(alea(rect.x0, rect.x1), alea(rect.y0, rect.y1))
1128
+ );
1129
+ }
1130
+
1131
+ spreadSetInRectangle(set, rect) {
1132
+ this.limitRectangle(rect);
1133
+ set.forEach((pp) =>
1134
+ pp.moveTo(alea(rect.x0, rect.x1), alea(rect.y0, rect.y1))
1135
+ );
1136
+ }
1137
+
1138
+ optimInitial() {
1139
+ const minx = -this.scalex / 2;
1140
+ const miny = -this.scaley / 2;
1141
+ const maxx = this.contWidth - 1.5 * this.scalex;
1142
+ const maxy = this.contHeight - 1.5 * this.scaley;
1143
+ let freex = this.contWidth - this.gameWidth;
1144
+ let freey = this.contHeight - this.gameHeight;
1145
+
1146
+ let where = [0, 0, 0, 0];
1147
+ let rects = [];
1148
+ if (freex > 1.5 * this.scalex) {
1149
+ where[1] = 1;
1150
+ rects[1] = {
1151
+ x0: this.gameWidth - 0.5 * this.scalex,
1152
+ x1: maxx,
1153
+ y0: miny,
1154
+ y1: maxy
1155
+ };
1156
+ }
1157
+ if (freex > 3 * this.scalex) {
1158
+ where[3] = 1;
1159
+ rects[3] = {
1160
+ x0: minx,
1161
+ x1: freex / 2 - 1.5 * this.scalex,
1162
+ y0: miny,
1163
+ y1: maxy
1164
+ };
1165
+ rects[1].x0 = this.contWidth - freex / 2 - 0.5 * this.scalex;
1166
+ }
1167
+ if (freey > 1.5 * this.scaley) {
1168
+ where[2] = 1;
1169
+ rects[2] = {
1170
+ x0: minx,
1171
+ x1: maxx,
1172
+ y0: this.gameHeight - 0.5 * this.scaley,
1173
+ y1: this.contHeight - 1.5 * this.scaley
1174
+ };
1175
+ }
1176
+ if (freey > 3 * this.scaley) {
1177
+ where[0] = 1;
1178
+ rects[0] = {
1179
+ x0: minx,
1180
+ x1: maxx,
1181
+ y0: miny,
1182
+ y1: freey / 2 - 1.5 * this.scaley
1183
+ };
1184
+ rects[2].y0 = this.contHeight - freey / 2 - 0.5 * this.scaley;
1185
+ }
1186
+ if (where.reduce((sum, a) => sum + a) < 2) {
1187
+ if (freex - freey > 0.2 * this.scalex || where[1]) {
1188
+ this.spreadInRectangle({
1189
+ x0: this.gameWidth - this.scalex / 2,
1190
+ x1: maxx,
1191
+ y0: miny,
1192
+ y1: maxy
1193
+ });
1194
+ } else if (freey - freex > 0.2 * this.scalex || where[2]) {
1195
+ this.spreadInRectangle({
1196
+ x0: minx,
1197
+ x1: maxx,
1198
+ y0: this.gameHeight - this.scaley / 2,
1199
+ y1: maxy
1200
+ });
1201
+ } else {
1202
+ if (this.gameWidth > this.gameHeight) {
1203
+ this.spreadInRectangle({
1204
+ x0: minx,
1205
+ x1: maxx,
1206
+ y0: this.gameHeight - this.scaley / 2,
1207
+ y1: maxy
1208
+ });
1209
+ } else {
1210
+ this.spreadInRectangle({
1211
+ x0: this.gameWidth - this.scalex / 2,
1212
+ x1: maxx,
1213
+ y0: miny,
1214
+ y1: maxy
1215
+ });
1216
+ }
1217
+ }
1218
+ return;
1219
+ }
1220
+ let nrects = [];
1221
+ rects.forEach((rect) => nrects.push(rect));
1222
+ let k0 = 0;
1223
+ const npTot = this.nx * this.ny;
1224
+ for (let k = 0; k < nrects.length; ++k) {
1225
+ let k1 = mround(((k + 1) / nrects.length) * npTot);
1226
+ this.spreadSetInRectangle(this.polyPieces.slice(k0, k1), nrects[k]);
1227
+ k0 = k1;
1228
+ }
1229
+ arrayShuffle(this.polyPieces);
1230
+ this.evaluateZIndex();
1231
+ }
1232
+
1233
+ evaluateZIndex() {
1234
+ for (let k = this.polyPieces.length - 1; k > 0; --k) {
1235
+ if (
1236
+ this.polyPieces[k].pieces.length > this.polyPieces[k - 1].pieces.length
1237
+ ) {
1238
+ [this.polyPieces[k], this.polyPieces[k - 1]] = [
1239
+ this.polyPieces[k - 1],
1240
+ this.polyPieces[k]
1241
+ ];
1242
+ }
1243
+ }
1244
+ this.polyPieces.forEach((pp, k) => {
1245
+ pp.canvas.style.zIndex = k + 10;
1246
+ });
1247
+ this.zIndexSup = this.polyPieces.length + 10;
1248
+ }
1249
+
1250
+ getStateData() {
1251
+ let ppData;
1252
+ let saved = { signature: FILE_SIGNATURE };
1253
+ if ("origin" in this.srcImage.dataset) {
1254
+ saved.origin = this.srcImage.dataset.origin;
1255
+ }
1256
+ saved.src = this.srcImage.src;
1257
+ let base = [
1258
+ this.nx,
1259
+ this.ny,
1260
+ this.scalex * this.nx,
1261
+ this.prng.seed,
1262
+ this.rotationAllowed ? 1 : 0,
1263
+ this.typeOfShape,
1264
+ this.srcImage.naturalWidth,
1265
+ this.srcImage.naturalHeight
1266
+ ];
1267
+ saved.base = base;
1268
+ let pps = [];
1269
+ base.push(pps);
1270
+ this.polyPieces.forEach((pp) => {
1271
+ ppData = [mround(pp.x), mround(pp.y)];
1272
+ if (this.rotationAllowed) ppData.push(pp.rot);
1273
+ pp.pieces.forEach((p) => ppData.push(p.kx, p.ky));
1274
+ pps.push(ppData);
1275
+ });
1276
+ return saved;
1277
+ }
1278
+ }
1279
+
1280
+ // JigsawPuzzle Wrapper Class
1281
+ // This will be appended to jigsaw-puzzle-class.js
1282
+
1283
+ // ============================================================================
1284
+ // CSS Injection - Library Styles
1285
+ // ============================================================================
1286
+
1287
+ /**
1288
+ * Injects the required CSS styles for the puzzle library
1289
+ * This ensures the library is self-contained and doesn't require external CSS
1290
+ */
1291
+ function injectPuzzleStyles() {
1292
+ // Check if styles have already been injected
1293
+ if (document.getElementById('jigsaw-puzzle-styles')) {
1294
+ return;
1295
+ }
1296
+
1297
+ const style = document.createElement('style');
1298
+ style.id = 'jigsaw-puzzle-styles';
1299
+ style.textContent = `
1300
+ /* Jigsaw Puzzle Library Styles */
1301
+ .polypiece {
1302
+ position: absolute;
1303
+ cursor: move;
1304
+ transition: transform 0.1s ease-out;
1305
+ }
1306
+
1307
+ .polypiece.moving {
1308
+ transition: left 1s ease-out, top 1s ease-out;
1309
+ }
1310
+
1311
+ .gameCanvas {
1312
+ position: absolute;
1313
+ pointer-events: none;
1314
+ }
1315
+ `;
1316
+
1317
+ document.head.appendChild(style);
1318
+ }
1319
+
1320
+ // Inject styles when module loads
1321
+ injectPuzzleStyles();
1322
+
1323
+ // ============================================================================
1324
+ // JigsawPuzzle - Main Public API Class
1325
+ // ============================================================================
1326
+
1327
+ export class JigsawPuzzle {
1328
+ /**
1329
+ * Creates a new JigsawPuzzle instance
1330
+ * @param {string|HTMLElement} containerId - ID of container div or element itself
1331
+ * @param {Object} options - Configuration options
1332
+ * @param {string} options.image - Image URL or data URL to use (ignored if savedData is provided)
1333
+ * @param {string|null} options.savedData - JSON string of saved game state:
1334
+ * - Non-empty string: use this saved data
1335
+ * - Empty string (""): load from localStorage
1336
+ * - Not provided or null: use normal initialization with options.image and other parameters
1337
+ * @param {number} options.numPieces - Number of puzzle pieces (default: 20, ignored if savedData is provided)
1338
+ * @param {number} options.shapeType - Shape type 0-3 (default: 0, ignored if savedData is provided)
1339
+ * @param {boolean} options.allowRotation - Allow piece rotation (default: false, ignored if savedData is provided)
1340
+ * @param {Function} options.onReady - Callback when puzzle is ready (image loaded, state 15)
1341
+ * @param {Function} options.onWin - Callback when puzzle is solved
1342
+ * @param {Function} options.onStart - Callback when game starts
1343
+ * @param {Function} options.onStop - Callback when game stops
1344
+ */
1345
+ constructor(containerId, options = {}) {
1346
+ const container = typeof containerId === "string"
1347
+ ? document.getElementById(containerId)
1348
+ : containerId;
1349
+
1350
+ if (!container) {
1351
+ throw new Error("Container element not found");
1352
+ }
1353
+
1354
+ // Store options
1355
+ this.options = {
1356
+ image: options.image || null,
1357
+ numPieces: options.numPieces || 20,
1358
+ shapeType: options.shapeType || 0,
1359
+ allowRotation: options.allowRotation || false,
1360
+ onReady: options.onReady || null,
1361
+ onWin: options.onWin || null,
1362
+ onStart: options.onStart || null,
1363
+ onStop: options.onStop || null
1364
+ };
1365
+
1366
+ // Create internal puzzle instance
1367
+ this.puzzle = new InternalPuzzle(container);
1368
+ this.puzzle.nbPieces = this.options.numPieces;
1369
+ this.puzzle.rotationAllowed = this.options.allowRotation;
1370
+ this.puzzle.typeOfShape = this.options.shapeType;
1371
+
1372
+ // Animation state
1373
+ this.events = [];
1374
+ this.state = 0;
1375
+ this.moving = {};
1376
+ this.tmpImage = null;
1377
+ this.lastMousePos = { x: 0, y: 0 };
1378
+ this.useMouse = true;
1379
+ this.playing = false;
1380
+ this.animationFrameId = null;
1381
+ this.restoredState = null;
1382
+ this.restoredString = "";
1383
+
1384
+ // Setup event handlers
1385
+ this._setupEventHandlers();
1386
+
1387
+ // Setup image load handler
1388
+ this.puzzle.srcImage.addEventListener("load", () => this._imageLoaded());
1389
+
1390
+ // Setup resize handler
1391
+ window.addEventListener("resize", () => {
1392
+ if (this.events.length && this.events[this.events.length - 1].event === "resize") return;
1393
+ this.events.push({ event: "resize" });
1394
+ });
1395
+
1396
+ // Handle saved data based on savedData option:
1397
+ // - Non-empty string: use that string
1398
+ // - Empty string: load from localStorage
1399
+ // - Not provided or null: use normal initialization
1400
+ let savedDataString = null;
1401
+
1402
+ if (options.savedData !== undefined && options.savedData !== null) {
1403
+ if (options.savedData === "") {
1404
+ // Empty string: load from localStorage
1405
+ try {
1406
+ savedDataString = localStorage.getItem("savepuzzle");
1407
+ } catch (exception) {
1408
+ // localStorage not available, continue with normal initialization
1409
+ }
1410
+ } else {
1411
+ // Non-empty string: use it directly
1412
+ savedDataString = options.savedData;
1413
+ }
1414
+ }
1415
+ // If savedData is not provided or null, savedDataString remains null and we use normal initialization
1416
+
1417
+ // If saved data found, parse and validate it
1418
+ if (savedDataString) {
1419
+ try {
1420
+ const parsedData = JSON.parse(savedDataString);
1421
+ // Validate signature and required fields
1422
+ if (parsedData.signature === FILE_SIGNATURE && parsedData.src) {
1423
+ this.restoredState = parsedData;
1424
+ // Set image from saved data (will override options.image)
1425
+ this.puzzle.imageLoaded = false;
1426
+ this.puzzle.srcImage.src = parsedData.src;
1427
+ if (parsedData.origin) {
1428
+ this.puzzle.srcImage.dataset.origin = parsedData.origin;
1429
+ }
1430
+ // Start restore flow - check if image is already loaded (cached)
1431
+ if (this.puzzle.srcImage.complete && this.puzzle.srcImage.naturalWidth > 0) {
1432
+ // Image already loaded, trigger load event manually
1433
+ this._imageLoaded();
1434
+ this.state = 158;
1435
+ } else {
1436
+ // Image will load asynchronously
1437
+ this.state = 158;
1438
+ }
1439
+ } else {
1440
+ // Invalid saved data, continue with normal initialization
1441
+ this.restoredState = null;
1442
+ if (this.options.image) {
1443
+ this.setImage(this.options.image);
1444
+ }
1445
+ }
1446
+ } catch (error) {
1447
+ // Invalid JSON, continue with normal initialization
1448
+ this.restoredState = null;
1449
+ if (this.options.image) {
1450
+ this.setImage(this.options.image);
1451
+ }
1452
+ }
1453
+ } else {
1454
+ // No saved data, load initial image if provided
1455
+ if (this.options.image) {
1456
+ this.setImage(this.options.image);
1457
+ }
1458
+ }
1459
+
1460
+ // Start animation loop
1461
+ this._animate(0);
1462
+ }
1463
+
1464
+ _setupEventHandlers() {
1465
+ const puzzle = this.puzzle;
1466
+ const container = puzzle.container;
1467
+
1468
+ container.addEventListener("mousedown", (event) => {
1469
+ this.useMouse = true;
1470
+ event.preventDefault();
1471
+ if (event.button !== 0) return;
1472
+ this.events.push({
1473
+ event: "touch",
1474
+ position: puzzle.relativeMouseCoordinates(event)
1475
+ });
1476
+ });
1477
+
1478
+ container.addEventListener("touchstart", (event) => {
1479
+ this.useMouse = false;
1480
+ event.preventDefault();
1481
+ if (event.touches.length === 0) return;
1482
+ const rTouch = [];
1483
+ for (let k = 0; k < event.touches.length; ++k) {
1484
+ rTouch[k] = puzzle.relativeMouseCoordinates(event.touches.item(k));
1485
+ }
1486
+ if (event.touches.length === 1) {
1487
+ this.events.push({ event: "touch", position: rTouch[0] });
1488
+ }
1489
+ if (event.touches.length === 2) {
1490
+ this.events.push({ event: "touches", touches: rTouch });
1491
+ }
1492
+ }, { passive: false });
1493
+
1494
+ const handleLeave = () => {
1495
+ this.events.push({ event: "leave" });
1496
+ };
1497
+
1498
+ container.addEventListener("mouseup", (event) => {
1499
+ this.useMouse = true;
1500
+ event.preventDefault();
1501
+ if (event.button !== 0) return;
1502
+ handleLeave();
1503
+ });
1504
+ container.addEventListener("touchend", handleLeave);
1505
+ container.addEventListener("touchleave", handleLeave);
1506
+ container.addEventListener("touchcancel", handleLeave);
1507
+
1508
+ container.addEventListener("mousemove", (event) => {
1509
+ this.useMouse = true;
1510
+ event.preventDefault();
1511
+ if (this.events.length && this.events[this.events.length - 1].event === "move")
1512
+ this.events.pop();
1513
+ const pos = puzzle.relativeMouseCoordinates(event);
1514
+ this.lastMousePos = pos;
1515
+ this.events.push({
1516
+ event: "move",
1517
+ position: pos,
1518
+ ev: event
1519
+ });
1520
+ });
1521
+
1522
+ container.addEventListener("touchmove", (event) => {
1523
+ this.useMouse = false;
1524
+ event.preventDefault();
1525
+ const rTouch = [];
1526
+ if (event.touches.length === 0) return;
1527
+ for (let k = 0; k < event.touches.length; ++k) {
1528
+ rTouch[k] = puzzle.relativeMouseCoordinates(event.touches.item(k));
1529
+ }
1530
+ if (event.touches.length === 1) {
1531
+ if (this.events.length && this.events[this.events.length - 1].event === "move")
1532
+ this.events.pop();
1533
+ this.events.push({ event: "move", position: rTouch[0] });
1534
+ }
1535
+ if (event.touches.length === 2) {
1536
+ if (this.events.length && this.events[this.events.length - 1].event === "moves")
1537
+ this.events.pop();
1538
+ this.events.push({ event: "moves", touches: rTouch });
1539
+ }
1540
+ }, { passive: false });
1541
+
1542
+ container.addEventListener("wheel", (event) => {
1543
+ this.useMouse = true;
1544
+ event.preventDefault();
1545
+ if (this.events.length && this.events[this.events.length - 1].event === "wheel")
1546
+ this.events.pop();
1547
+ this.events.push({ event: "wheel", wheel: event });
1548
+ });
1549
+ }
1550
+
1551
+ _imageLoaded() {
1552
+ this.puzzle.imageLoaded = true;
1553
+ let event = { event: "srcImageLoaded" };
1554
+ if (this.restoredState) {
1555
+ if (
1556
+ mround(this.puzzle.srcImage.naturalWidth) !== this.restoredState.base[6] ||
1557
+ mround(this.puzzle.srcImage.naturalHeight) !== this.restoredState.base[7]
1558
+ ) {
1559
+ event.event = "wrongImage";
1560
+ this.restoredState = null;
1561
+ }
1562
+ }
1563
+ this.events.push(event);
1564
+ }
1565
+
1566
+ _animate(tStamp) {
1567
+ this.animationFrameId = requestAnimationFrame((ts) => this._animate(ts));
1568
+
1569
+ let event;
1570
+ if (this.events.length) event = this.events.shift();
1571
+ if (event && event.event === "reset") this.state = 0;
1572
+
1573
+ // Resize event
1574
+ if (event && event.event === "resize") {
1575
+ const puzzle = this.puzzle;
1576
+ const prevWidth = puzzle.contWidth;
1577
+ const prevHeight = puzzle.contHeight;
1578
+ puzzle.getContainerSize();
1579
+ if (this.state === 15 || this.state === 60) {
1580
+ fitImage(this.tmpImage, puzzle.contWidth * 0.95, puzzle.contHeight * 0.95);
1581
+ } else if (this.state >= 25) {
1582
+ const prevGameWidth = puzzle.gameWidth;
1583
+ const prevGameHeight = puzzle.gameHeight;
1584
+ puzzle.scale();
1585
+ const reScale = puzzle.contWidth / prevWidth;
1586
+ puzzle.polyPieces.forEach((pp) => {
1587
+ let nx = puzzle.contWidth / 2 - (prevWidth / 2 - pp.x) * reScale;
1588
+ let ny = puzzle.contHeight / 2 - (prevHeight / 2 - pp.y) * reScale;
1589
+ nx = mmin(mmax(nx, -puzzle.scalex / 2), puzzle.contWidth - 1.5 * puzzle.scalex);
1590
+ ny = mmin(mmax(ny, -puzzle.scaley / 2), puzzle.contHeight - 1.5 * puzzle.scaley);
1591
+ pp.moveTo(nx, ny);
1592
+ pp.drawImage();
1593
+ });
1594
+ }
1595
+ return;
1596
+ }
1597
+
1598
+ switch (this.state) {
1599
+ case 0:
1600
+ this.state = 10;
1601
+ // fall through
1602
+
1603
+ case 10:
1604
+ this.playing = false;
1605
+ if (!this.puzzle.imageLoaded) return;
1606
+ this.puzzle.container.innerHTML = "";
1607
+ this.tmpImage = document.createElement("img");
1608
+ this.tmpImage.src = this.puzzle.srcImage.src;
1609
+ this.puzzle.getContainerSize();
1610
+ fitImage(this.tmpImage, this.puzzle.contWidth * 0.95, this.puzzle.contHeight * 0.95);
1611
+ this.tmpImage.style.boxShadow = "-4px 4px 4px rgba(0, 0, 0, 0.5)";
1612
+ this.puzzle.container.appendChild(this.tmpImage);
1613
+ this.state = 15;
1614
+ // Call onReady callback when puzzle is ready (image loaded and displayed)
1615
+ if (this.options.onReady) {
1616
+ this.options.onReady();
1617
+ }
1618
+ break;
1619
+
1620
+ case 15:
1621
+ this.playing = false;
1622
+ if (!event) return;
1623
+ if (event.event === "nbpieces") {
1624
+ this.puzzle.nbPieces = event.nbpieces;
1625
+ this.state = 20;
1626
+ } else if (event.event === "srcImageLoaded") {
1627
+ this.state = 10;
1628
+ return;
1629
+ } else return;
1630
+
1631
+ case 20:
1632
+ this.playing = true;
1633
+ if (this.options.onStart) this.options.onStart();
1634
+ this.puzzle.rotationAllowed = this.options.allowRotation;
1635
+ if (this.restoredState) {
1636
+ this.puzzle.create(this.restoredState.base);
1637
+ } else {
1638
+ this.puzzle.create();
1639
+ }
1640
+ if (this.restoredState) {
1641
+ this.puzzle.doScale(this.restoredState.base[2]);
1642
+ } else {
1643
+ this.puzzle.scale();
1644
+ }
1645
+ this.puzzle.polyPieces.forEach((pp) => {
1646
+ pp.drawImage();
1647
+ if (this.restoredState) {
1648
+ pp.moveTo(pp.x, pp.y);
1649
+ } else {
1650
+ pp.moveToInitialPlace();
1651
+ }
1652
+ });
1653
+ this.puzzle.gameCanvas.style.top = this.puzzle.offsy + "px";
1654
+ this.puzzle.gameCanvas.style.left = this.puzzle.offsx + "px";
1655
+ this.puzzle.gameCanvas.style.display = "none";
1656
+ this.state = 25;
1657
+ if (this.restoredState) {
1658
+ this.restoredState = null;
1659
+ this.state = 50;
1660
+ }
1661
+ break;
1662
+
1663
+ case 25:
1664
+ this.puzzle.gameCanvas.style.display = "none";
1665
+ this.puzzle.polyPieces.forEach((pp) => {
1666
+ pp.canvas.classList.add("moving");
1667
+ });
1668
+ this.state = 30;
1669
+ break;
1670
+
1671
+ case 30:
1672
+ this.puzzle.optimInitial();
1673
+ setTimeout(() => this.events.push({ event: "finished" }), 1200);
1674
+ this.state = 35;
1675
+ break;
1676
+
1677
+ case 35:
1678
+ if (!event || event.event !== "finished") return;
1679
+ this.puzzle.polyPieces.forEach((pp) => {
1680
+ pp.canvas.classList.remove("moving");
1681
+ });
1682
+ this.state = 50;
1683
+ break;
1684
+
1685
+ case 50:
1686
+ if (!event) return;
1687
+ if (event.event === "stop") {
1688
+ this.state = 10;
1689
+ return;
1690
+ }
1691
+ if (event.event === "nbpieces") {
1692
+ this.puzzle.nbPieces = event.nbpieces;
1693
+ this.state = 20;
1694
+ } else if (event.event === "touch") {
1695
+ this.moving = {
1696
+ xMouseInit: event.position.x,
1697
+ yMouseInit: event.position.y,
1698
+ tInit: tStamp
1699
+ };
1700
+ for (let k = this.puzzle.polyPieces.length - 1; k >= 0; --k) {
1701
+ let pp = this.puzzle.polyPieces[k];
1702
+ if (pp.isPointInPath(event.position)) {
1703
+ pp.selected = true;
1704
+ pp.drawImage();
1705
+ this.moving.pp = pp;
1706
+ this.moving.ppXInit = pp.x;
1707
+ this.moving.ppYInit = pp.y;
1708
+ this.puzzle.polyPieces.splice(k, 1);
1709
+ this.puzzle.polyPieces.push(pp);
1710
+ pp.canvas.style.zIndex = this.puzzle.zIndexSup;
1711
+ this.state = 55;
1712
+ return;
1713
+ }
1714
+ }
1715
+ this.state = 100;
1716
+ } else if (event.event === "touches") {
1717
+ this.moving = { touches: event.touches };
1718
+ this.state = 110;
1719
+ } else if (event.event === "wheel") {
1720
+ if (event.wheel.deltaY > 0) this.puzzle.zoomBy(1.3, this.lastMousePos);
1721
+ if (event.wheel.deltaY < 0) this.puzzle.zoomBy(1 / 1.3, this.lastMousePos);
1722
+ }
1723
+ break;
1724
+
1725
+ case 55:
1726
+ if (!event) return;
1727
+ if (event.event === "stop") {
1728
+ this.state = 10;
1729
+ return;
1730
+ }
1731
+ switch (event.event) {
1732
+ case "moves":
1733
+ case "touches":
1734
+ this.moving.pp.selected = false;
1735
+ this.moving.pp.drawImage();
1736
+ this.moving = { touches: event.touches };
1737
+ this.state = 110;
1738
+ break;
1739
+ case "move":
1740
+ if (event?.ev?.buttons === 0) {
1741
+ this.events.push({ event: "leave" });
1742
+ break;
1743
+ }
1744
+ this.moving.pp.moveTo(
1745
+ event.position.x - this.moving.xMouseInit + this.moving.ppXInit,
1746
+ event.position.y - this.moving.yMouseInit + this.moving.ppYInit
1747
+ );
1748
+ break;
1749
+ case "leave":
1750
+ if (this.puzzle.rotationAllowed && tStamp < this.moving.tInit + 250) {
1751
+ this.moving.pp.rotate((this.moving.pp.rot + 1) % 4);
1752
+ this.moving.pp.coerceToContainer();
1753
+ }
1754
+ this.moving.pp.selected = false;
1755
+ this.moving.pp.drawImage();
1756
+ let merged = false;
1757
+ let doneSomething;
1758
+ do {
1759
+ doneSomething = false;
1760
+ for (let k = this.puzzle.polyPieces.length - 1; k >= 0; --k) {
1761
+ let pp = this.puzzle.polyPieces[k];
1762
+ if (pp === this.moving.pp) continue;
1763
+ if (this.moving.pp.ifNear(pp)) {
1764
+ merged = true;
1765
+ if (pp.pieces.length > this.moving.pp.pieces.length) {
1766
+ pp.merge(this.moving.pp);
1767
+ this.moving.pp = pp;
1768
+ } else {
1769
+ this.moving.pp.merge(pp);
1770
+ }
1771
+ doneSomething = true;
1772
+ break;
1773
+ }
1774
+ }
1775
+ } while (doneSomething);
1776
+ this.puzzle.evaluateZIndex();
1777
+ if (merged) {
1778
+ this.moving.pp.selected = true;
1779
+ this.moving.pp.drawImage(true);
1780
+ this.moving.tInit = tStamp + 500;
1781
+ this.state = 56;
1782
+ break;
1783
+ }
1784
+ this.state = 50;
1785
+ if (this.puzzle.polyPieces.length === 1 && this.puzzle.polyPieces[0].rot === 0) {
1786
+ this.state = 60;
1787
+ }
1788
+ }
1789
+ break;
1790
+
1791
+ case 56:
1792
+ if (tStamp < this.moving.tInit) return;
1793
+ this.moving.pp.selected = false;
1794
+ this.moving.pp.drawImage();
1795
+ if (this.puzzle.polyPieces.length === 1 && this.puzzle.polyPieces[0].rot === 0)
1796
+ this.state = 60;
1797
+ else
1798
+ this.state = 50;
1799
+ break;
1800
+
1801
+ case 60:
1802
+ this.playing = false;
1803
+ if (this.options.onWin) this.options.onWin();
1804
+ this.puzzle.container.innerHTML = "";
1805
+ this.puzzle.getContainerSize();
1806
+ fitImage(this.tmpImage, this.puzzle.contWidth * 0.95, this.puzzle.contHeight * 0.95);
1807
+ const finalWidth = this.tmpImage.style.width;
1808
+ const finalHeight = this.tmpImage.style.height;
1809
+ this.tmpImage.style.width = `${this.puzzle.nx * this.puzzle.scalex}px`;
1810
+ this.tmpImage.style.height = `${this.puzzle.ny * this.puzzle.scaley}px`;
1811
+ this.tmpImage.style.left = `${((this.puzzle.polyPieces[0].x + this.puzzle.scalex / 2 + this.puzzle.gameWidth / 2) / this.puzzle.contWidth) * 100}%`;
1812
+ this.tmpImage.style.top = `${((this.puzzle.polyPieces[0].y + this.puzzle.scaley / 2 + this.puzzle.gameHeight / 2) / this.puzzle.contHeight) * 100}%`;
1813
+ this.tmpImage.style.boxShadow = "-4px 4px 4px rgba(0, 0, 0, 0.5)";
1814
+ this.tmpImage.classList.add("moving");
1815
+ setTimeout(() => {
1816
+ this.tmpImage.style.top = this.tmpImage.style.left = "50%";
1817
+ this.tmpImage.style.width = finalWidth;
1818
+ this.tmpImage.style.height = finalHeight;
1819
+ }, 0);
1820
+ this.puzzle.container.appendChild(this.tmpImage);
1821
+ this.state = 15;
1822
+ break;
1823
+
1824
+ case 100:
1825
+ if (!event) return;
1826
+ if (event.event === "move") {
1827
+ if (event?.ev?.buttons === 0) {
1828
+ this.state = 50;
1829
+ break;
1830
+ }
1831
+ this.puzzle.sweepBy(
1832
+ event.position.x - this.moving.xMouseInit,
1833
+ event.position.y - this.moving.yMouseInit
1834
+ );
1835
+ this.moving.xMouseInit = event.position.x;
1836
+ this.moving.yMouseInit = event.position.y;
1837
+ return;
1838
+ }
1839
+ if (event.event === "leave") {
1840
+ this.state = 50;
1841
+ return;
1842
+ }
1843
+ if (event.event === "touches") {
1844
+ this.moving = { touches: event.touches };
1845
+ this.state = 110;
1846
+ }
1847
+ break;
1848
+
1849
+ case 110:
1850
+ if (!event) return;
1851
+ if (event.event === "leave") {
1852
+ this.state = 50;
1853
+ return;
1854
+ }
1855
+ if (event.event === "moves") {
1856
+ const center = {
1857
+ x: (this.moving.touches[0].x + this.moving.touches[1].x) / 2,
1858
+ y: (this.moving.touches[0].y + this.moving.touches[1].y) / 2
1859
+ };
1860
+ const dInit = mhypot(
1861
+ this.moving.touches[0].x - this.moving.touches[1].x,
1862
+ this.moving.touches[0].y - this.moving.touches[1].y
1863
+ );
1864
+ const d = mhypot(
1865
+ event.touches[0].x - event.touches[1].x,
1866
+ event.touches[0].y - event.touches[1].y
1867
+ );
1868
+ const dRef = msqrt(this.puzzle.contWidth * this.puzzle.contHeight) / 5;
1869
+ this.puzzle.zoomBy(Math.exp((d - dInit) / dRef), center);
1870
+ this.moving.touches = event.touches;
1871
+ return;
1872
+ }
1873
+ break;
1874
+
1875
+ case 158:
1876
+ if (event && event.event === "srcImageLoaded") {
1877
+ this.state = 160;
1878
+ } else if (event && event.event === "wrongImage") {
1879
+ this.state = 10;
1880
+ }
1881
+ break;
1882
+
1883
+ case 160:
1884
+ this.tmpImage.src = this.puzzle.srcImage.src;
1885
+ fitImage(this.tmpImage, this.puzzle.contWidth * 0.95, this.puzzle.contHeight * 0.95);
1886
+ this.state = 20;
1887
+ break;
1888
+ }
1889
+ }
1890
+
1891
+ // ============================================================================
1892
+ // Public API Methods
1893
+ // ============================================================================
1894
+
1895
+ /**
1896
+ * Start a new game with the current settings
1897
+ */
1898
+ start() {
1899
+ this.events.push({ event: "nbpieces", nbpieces: this.options.numPieces });
1900
+ }
1901
+
1902
+ /**
1903
+ * Stop the current game
1904
+ */
1905
+ stop() {
1906
+ this.playing = false;
1907
+ if (this.options.onStop) this.options.onStop();
1908
+ this.events.push({ event: "stop" });
1909
+ }
1910
+
1911
+ /**
1912
+ * Reset the puzzle to initial state
1913
+ * Use this to start a new game with the same instance and container
1914
+ * The puzzle will reload the current image and be ready for a new game
1915
+ */
1916
+ reset() {
1917
+ this.events.push({ event: "reset" });
1918
+ this.state = 0;
1919
+ this.playing = false;
1920
+ this.restoredState = null;
1921
+ this.restoredString = "";
1922
+ }
1923
+
1924
+ /**
1925
+ * Save the current game state
1926
+ * @param {Function} callback - Optional callback to receive saved data as JSON string
1927
+ */
1928
+ save(callback) {
1929
+ // Get state data and convert to string
1930
+ const savedData = this.puzzle.getStateData();
1931
+ const savedString = JSON.stringify(savedData);
1932
+
1933
+ // If callback provided, call it with the string; otherwise save to localStorage
1934
+ if (callback) {
1935
+ callback(savedString);
1936
+ } else {
1937
+ try {
1938
+ localStorage.setItem("savepuzzle", savedString);
1939
+ } catch (exception) {
1940
+ console.error("Failed to save to localStorage:", exception);
1941
+ }
1942
+ }
1943
+ }
1944
+
1945
+
1946
+ /**
1947
+ * Set the puzzle image
1948
+ * @param {string} imageUrl - URL or data URL of the image
1949
+ */
1950
+ setImage(imageUrl) {
1951
+ this.options.image = imageUrl;
1952
+ this.puzzle.imageLoaded = false;
1953
+ this.puzzle.srcImage.src = imageUrl;
1954
+ delete this.puzzle.srcImage.dataset.origin;
1955
+ }
1956
+
1957
+ /**
1958
+ * Update puzzle options
1959
+ * @param {Object} newOptions - Options to update
1960
+ */
1961
+ setOptions(newOptions) {
1962
+ Object.assign(this.options, newOptions);
1963
+ if (newOptions.numPieces !== undefined) {
1964
+ this.puzzle.nbPieces = newOptions.numPieces;
1965
+ }
1966
+ if (newOptions.allowRotation !== undefined) {
1967
+ this.puzzle.rotationAllowed = newOptions.allowRotation;
1968
+ }
1969
+ if (newOptions.shapeType !== undefined) {
1970
+ this.puzzle.typeOfShape = newOptions.shapeType;
1971
+ }
1972
+ }
1973
+
1974
+ /**
1975
+ * Destroy the puzzle instance and clean up completely
1976
+ * Use this when you want to remove the puzzle entirely and create a new one
1977
+ * For reusing the same instance with a new game, use reset() instead
1978
+ */
1979
+ destroy() {
1980
+ // Stop animation loop
1981
+ if (this.animationFrameId) {
1982
+ cancelAnimationFrame(this.animationFrameId);
1983
+ this.animationFrameId = null;
1984
+ }
1985
+
1986
+ // Clear events
1987
+ this.events = [];
1988
+ this.playing = false;
1989
+
1990
+ // Clear container (removes all puzzle elements)
1991
+ if (this.puzzle && this.puzzle.container) {
1992
+ this.puzzle.container.innerHTML = "";
1993
+ }
1994
+
1995
+ // Note: Event listeners are not removed because they're bound to the container
1996
+ // If you need to completely remove listeners, you'd need to store references
1997
+ // For most use cases, clearing innerHTML and stopping animation is sufficient
1998
+ }
1961
1999
  }