jigsawpuzzlegame 1.0.10 → 1.0.12

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.
package/README.md CHANGED
@@ -61,6 +61,9 @@ Make sure your container has a defined size:
61
61
  | `onWin` | function | `null` | Callback function called when the puzzle is completed |
62
62
  | `onStart` | function | `null` | Callback function called when a game starts |
63
63
  | `onStop` | function | `null` | Callback function called when a game is stopped |
64
+ | `onMerged` | function | `null` | Callback function called when puzzle pieces are merged together (receives the merged piece as parameter) |
65
+ | `onChanged` | function | `null` | Callback function called when a piece is moved or changed (receives the piece as parameter) |
66
+ | `onDeleted` | function | `null` | Callback function called when a piece is deleted/merged into another piece (receives the deleted piece as parameter) |
64
67
 
65
68
  ## API Methods
66
69
 
@@ -196,6 +199,15 @@ const puzzle = new JigsawPuzzle('puzzle-container', {
196
199
  },
197
200
  onStop: () => {
198
201
  console.log('Game stopped');
202
+ },
203
+ onMerged: (piece) => {
204
+ console.log('Pieces merged!', piece);
205
+ },
206
+ onChanged: (piece) => {
207
+ console.log('Piece moved', piece);
208
+ },
209
+ onDeleted: (piece) => {
210
+ console.log('Piece deleted', piece);
199
211
  }
200
212
  });
201
213
 
package/favicon.ico ADDED
Binary file
package/game.css ADDED
@@ -0,0 +1,23 @@
1
+ /* Demo-specific styles for game.html */
2
+ /* Note: Library styles (.polypiece, .gameCanvas) are automatically injected by jigsaw-puzzle-game.js */
3
+
4
+ * {
5
+ margin: 0;
6
+ padding: 0;
7
+ box-sizing: border-box;
8
+ }
9
+
10
+ body {
11
+ width: 100vw;
12
+ height: 100vh;
13
+ overflow: hidden;
14
+ background-color: #f0f0f0;
15
+ }
16
+
17
+ #puzzle-container {
18
+ width: 100%;
19
+ height: 100%;
20
+ position: relative;
21
+ background-color: #e8e8e8;
22
+ }
23
+
package/game.html ADDED
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <link rel="icon" href="favicon.ico" type="image/x-icon">
7
+ <title>Jigsaw Puzzle Game</title>
8
+ <link rel="stylesheet" href="game.css">
9
+ </head>
10
+ <body>
11
+ <div id="puzzle-container"></div>
12
+ <script type="module" src="game.js"></script>
13
+ </body>
14
+ </html>
15
+
package/game.js ADDED
@@ -0,0 +1,28 @@
1
+ import { JigsawPuzzle } from './jigsaw-puzzle-game.js';
2
+
3
+ // Default image URL - you can change this to any image URL
4
+ const DEFAULT_IMAGE = 'https://assets.codepen.io/2574552/Mona_Lisa.jpg';
5
+
6
+ // Initialize the puzzle
7
+ const puzzle = new JigsawPuzzle('puzzle-container', {
8
+ image: DEFAULT_IMAGE,
9
+ numPieces: 25,
10
+ shapeType: 0,
11
+ allowRotation: true,
12
+ onReady: () => {
13
+ // Puzzle is ready (image loaded), start the game
14
+ console.log('Puzzle ready, starting game...');
15
+ puzzle.start();
16
+ },
17
+ onWin: () => {
18
+ console.log('Congratulations! Puzzle solved!');
19
+ alert('Congratulations! You solved the puzzle!');
20
+ },
21
+ onStart: () => {
22
+ console.log('Game started');
23
+ },
24
+ onStop: () => {
25
+ console.log('Game stopped');
26
+ }
27
+ });
28
+
package/index.html ADDED
@@ -0,0 +1,718 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <link rel="icon" href="favicon.ico" type="image/x-icon">
7
+ <title>JigsawPuzzle Class - Usage Guide</title>
8
+ <style>
9
+ body {
10
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
11
+ line-height: 1.6;
12
+ max-width: 1000px;
13
+ margin: 0 auto;
14
+ padding: 20px;
15
+ background-color: #f5f5f5;
16
+ }
17
+ h1 {
18
+ color: #333;
19
+ border-bottom: 3px solid #4CAF50;
20
+ padding-bottom: 10px;
21
+ }
22
+ h2 {
23
+ color: #555;
24
+ margin-top: 30px;
25
+ border-bottom: 2px solid #ddd;
26
+ padding-bottom: 5px;
27
+ }
28
+ h3 {
29
+ color: #666;
30
+ margin-top: 20px;
31
+ }
32
+ code {
33
+ background-color: #f4f4f4;
34
+ padding: 2px 6px;
35
+ border-radius: 3px;
36
+ font-family: 'Courier New', monospace;
37
+ font-size: 0.9em;
38
+ }
39
+ pre {
40
+ background-color: #2d2d2d;
41
+ color: #f8f8f2;
42
+ padding: 15px;
43
+ border-radius: 5px;
44
+ overflow-x: auto;
45
+ margin: 15px 0;
46
+ }
47
+ pre code {
48
+ background-color: transparent;
49
+ padding: 0;
50
+ color: inherit;
51
+ }
52
+ .option-table {
53
+ width: 100%;
54
+ border-collapse: collapse;
55
+ margin: 15px 0;
56
+ background-color: white;
57
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
58
+ }
59
+ .option-table th,
60
+ .option-table td {
61
+ padding: 12px;
62
+ text-align: left;
63
+ border-bottom: 1px solid #ddd;
64
+ }
65
+ .option-table th {
66
+ background-color: #4CAF50;
67
+ color: white;
68
+ font-weight: bold;
69
+ }
70
+ .option-table tr:hover {
71
+ background-color: #f9f9f9;
72
+ }
73
+ .method-section {
74
+ background-color: white;
75
+ padding: 15px;
76
+ margin: 15px 0;
77
+ border-left: 4px solid #4CAF50;
78
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
79
+ }
80
+ .example-box {
81
+ background-color: #e8f5e9;
82
+ border-left: 4px solid #4CAF50;
83
+ padding: 15px;
84
+ margin: 15px 0;
85
+ }
86
+ .warning-box {
87
+ background-color: #fff3cd;
88
+ border-left: 4px solid #ffc107;
89
+ padding: 15px;
90
+ margin: 15px 0;
91
+ }
92
+ ul {
93
+ padding-left: 20px;
94
+ }
95
+ li {
96
+ margin: 5px 0;
97
+ }
98
+ .button-container {
99
+ text-align: center;
100
+ margin-bottom: 30px;
101
+ }
102
+ .button-container a {
103
+ display: inline-block;
104
+ background-color: #4CAF50;
105
+ color: white;
106
+ padding: 12px 24px;
107
+ text-decoration: none;
108
+ border-radius: 5px;
109
+ font-weight: bold;
110
+ font-size: 16px;
111
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
112
+ margin: 5px;
113
+ min-width: 150px;
114
+ text-align: center;
115
+ box-sizing: border-box;
116
+ }
117
+ .button-container a:hover {
118
+ background-color: #45a049;
119
+ }
120
+ @media (max-width: 600px) {
121
+ body {
122
+ padding: 10px;
123
+ }
124
+ .button-container a {
125
+ display: block;
126
+ width: 100%;
127
+ max-width: 100%;
128
+ min-width: auto;
129
+ margin: 8px 0;
130
+ }
131
+ h1 {
132
+ font-size: 1.5em;
133
+ }
134
+ h2 {
135
+ font-size: 1.3em;
136
+ }
137
+ pre {
138
+ font-size: 0.85em;
139
+ padding: 10px;
140
+ }
141
+ .option-table th,
142
+ .option-table td {
143
+ padding: 8px;
144
+ font-size: 0.9em;
145
+ }
146
+ }
147
+ </style>
148
+ </head>
149
+ <body>
150
+ <div class="button-container">
151
+ <a href="game.html">Demo</a>
152
+ <a href="jigsaw-puzzle-game.js" download="jigsaw-puzzle-game.js">Download Source</a>
153
+ </div>
154
+
155
+ <h1>JigsawPuzzle Class - Usage Guide</h1>
156
+
157
+ <p>This guide explains how to use the <code>JigsawPuzzle</code> class to create interactive jigsaw puzzle games in your web application.</p>
158
+
159
+ <h2>1. Importing the Class</h2>
160
+
161
+ <p>The class is exported as an ES6 module. Import it in your JavaScript file:</p>
162
+
163
+ <pre><code>import { JigsawPuzzle } from './jigsaw-puzzle-game.js';</code></pre>
164
+
165
+ <p><strong>Note:</strong> Make sure your HTML file uses <code>type="module"</code> in the script tag:</p>
166
+
167
+ <pre><code>&lt;script type="module" src="your-script.js"&gt;&lt;/script&gt;</code></pre>
168
+
169
+ <h2>2. Basic Setup</h2>
170
+
171
+ <p>You need an HTML container element where the puzzle will be displayed:</p>
172
+
173
+ <pre><code>&lt;div id="puzzle-container"&gt;&lt;/div&gt;</code></pre>
174
+
175
+ <p>Then create a new puzzle instance:</p>
176
+
177
+ <pre><code>const puzzle = new JigsawPuzzle('puzzle-container', {
178
+ image: 'https://example.com/image.jpg',
179
+ numPieces: 20,
180
+ shapeType: 0,
181
+ allowRotation: false
182
+ });</code></pre>
183
+
184
+ <h2>3. Configuration Options</h2>
185
+
186
+ <p>The constructor takes two parameters:</p>
187
+ <ul>
188
+ <li><code>containerId</code> - String ID of the container element, or the DOM element itself</li>
189
+ <li><code>options</code> - Configuration object (see table below)</li>
190
+ </ul>
191
+
192
+ <table class="option-table">
193
+ <thead>
194
+ <tr>
195
+ <th>Option</th>
196
+ <th>Type</th>
197
+ <th>Default</th>
198
+ <th>Description</th>
199
+ </tr>
200
+ </thead>
201
+ <tbody>
202
+ <tr>
203
+ <td><code>image</code></td>
204
+ <td>string</td>
205
+ <td><code>null</code></td>
206
+ <td>URL or data URL of the image to use for the puzzle. Can be set later with <code>setImage()</code>. Ignored if <code>savedData</code> is provided.</td>
207
+ </tr>
208
+ <tr>
209
+ <td><code>savedData</code></td>
210
+ <td>string\|null</td>
211
+ <td><code>null</code></td>
212
+ <td>JSON string of saved game state:
213
+ <ul style="margin: 5px 0; padding-left: 20px;">
214
+ <li>Non-empty string: use this saved data</li>
215
+ <li>Empty string (<code>""</code>): load from localStorage</li>
216
+ <li>Not provided or <code>null</code>: use normal initialization with <code>image</code> and other parameters</li>
217
+ </ul>
218
+ </td>
219
+ </tr>
220
+ <tr>
221
+ <td><code>numPieces</code></td>
222
+ <td>number</td>
223
+ <td><code>20</code></td>
224
+ <td>Number of puzzle pieces (approximate - actual count depends on optimal grid layout). Ignored if <code>savedData</code> is provided.</td>
225
+ </tr>
226
+ <tr>
227
+ <td><code>shapeType</code></td>
228
+ <td>number</td>
229
+ <td><code>0</code></td>
230
+ <td>Shape type for puzzle pieces (0-3, ignored if <code>savedData</code> is provided):
231
+ <ul style="margin: 5px 0; padding-left: 20px;">
232
+ <li><code>0</code> - Classic jigsaw shape (curved tabs)</li>
233
+ <li><code>1</code> - Alternative shape 1</li>
234
+ <li><code>2</code> - Alternative shape 2</li>
235
+ <li><code>3</code> - Straight edges (rectangular pieces)</li>
236
+ </ul>
237
+ </td>
238
+ </tr>
239
+ <tr>
240
+ <td><code>allowRotation</code></td>
241
+ <td>boolean</td>
242
+ <td><code>false</code></td>
243
+ <td>Whether pieces can be rotated by clicking/tapping (90° increments). Ignored if <code>savedData</code> is provided.</td>
244
+ </tr>
245
+ <tr>
246
+ <td><code>onReady</code></td>
247
+ <td>function</td>
248
+ <td><code>null</code></td>
249
+ <td>Callback function called when the puzzle is ready (image loaded and displayed). Use this to start the game.</td>
250
+ </tr>
251
+ <tr>
252
+ <td><code>onWin</code></td>
253
+ <td>function</td>
254
+ <td><code>null</code></td>
255
+ <td>Callback function called when the puzzle is completed</td>
256
+ </tr>
257
+ <tr>
258
+ <td><code>onStart</code></td>
259
+ <td>function</td>
260
+ <td><code>null</code></td>
261
+ <td>Callback function called when a game starts</td>
262
+ </tr>
263
+ <tr>
264
+ <td><code>onStop</code></td>
265
+ <td>function</td>
266
+ <td><code>null</code></td>
267
+ <td>Callback function called when a game is stopped</td>
268
+ </tr>
269
+ <tr>
270
+ <td><code>onMerged</code></td>
271
+ <td>function</td>
272
+ <td><code>null</code></td>
273
+ <td>Callback function called when puzzle pieces are merged together (receives the merged piece as parameter)</td>
274
+ </tr>
275
+ <tr>
276
+ <td><code>onChanged</code></td>
277
+ <td>function</td>
278
+ <td><code>null</code></td>
279
+ <td>Callback function called when a piece is moved or changed (receives the piece as parameter)</td>
280
+ </tr>
281
+ <tr>
282
+ <td><code>onDeleted</code></td>
283
+ <td>function</td>
284
+ <td><code>null</code></td>
285
+ <td>Callback function called when a piece is deleted/merged into another piece (receives the deleted piece as parameter)</td>
286
+ </tr>
287
+ </tbody>
288
+ </table>
289
+
290
+ <h2>4. Event Callbacks</h2>
291
+
292
+ <p>All callbacks are optional and receive no parameters:</p>
293
+
294
+ <div class="method-section">
295
+ <h3><code>onReady()</code></h3>
296
+ <p>Called when the puzzle image has loaded and the puzzle is ready to start. This is the perfect time to call <code>puzzle.start()</code>.</p>
297
+ <div class="example-box">
298
+ <strong>Example:</strong>
299
+ <pre><code>onReady: () => {
300
+ console.log('Puzzle ready!');
301
+ puzzle.start();
302
+ }</code></pre>
303
+ </div>
304
+ </div>
305
+
306
+ <div class="method-section">
307
+ <h3><code>onStart()</code></h3>
308
+ <p>Called when a game actually starts (pieces are being created and distributed).</p>
309
+ </div>
310
+
311
+ <div class="method-section">
312
+ <h3><code>onWin()</code></h3>
313
+ <p>Called when the puzzle is completed (all pieces connected and in correct position).</p>
314
+ <div class="example-box">
315
+ <strong>Example:</strong>
316
+ <pre><code>onWin: () => {
317
+ alert('Congratulations! You solved the puzzle!');
318
+ // Could show a victory screen, update score, etc.
319
+ }</code></pre>
320
+ </div>
321
+ </div>
322
+
323
+ <div class="method-section">
324
+ <h3><code>onStop()</code></h3>
325
+ <p>Called when a game is stopped (when <code>stop()</code> is called).</p>
326
+ </div>
327
+
328
+ <div class="method-section">
329
+ <h3><code>onMerged(piece)</code></h3>
330
+ <p>Called when puzzle pieces are merged together. Receives the merged piece as a parameter.</p>
331
+ <div class="example-box">
332
+ <strong>Example:</strong>
333
+ <pre><code>onMerged: (piece) => {
334
+ console.log('Pieces merged!', piece);
335
+ // Could update UI, play sound, etc.
336
+ }</code></pre>
337
+ </div>
338
+ </div>
339
+
340
+ <div class="method-section">
341
+ <h3><code>onChanged(piece)</code></h3>
342
+ <p>Called when a piece is moved or changed. Receives the piece as a parameter.</p>
343
+ <div class="example-box">
344
+ <strong>Example:</strong>
345
+ <pre><code>onChanged: (piece) => {
346
+ console.log('Piece moved', piece);
347
+ // Could track moves, update statistics, etc.
348
+ }</code></pre>
349
+ </div>
350
+ </div>
351
+
352
+ <div class="method-section">
353
+ <h3><code>onDeleted(piece)</code></h3>
354
+ <p>Called when a piece is deleted/merged into another piece. Receives the deleted piece as a parameter.</p>
355
+ <div class="example-box">
356
+ <strong>Example:</strong>
357
+ <pre><code>onDeleted: (piece) => {
358
+ console.log('Piece deleted', piece);
359
+ // Could track merges, update UI, etc.
360
+ }</code></pre>
361
+ </div>
362
+ </div>
363
+
364
+ <h2>5. Public Methods</h2>
365
+
366
+ <div class="method-section">
367
+ <h3><code>start()</code></h3>
368
+ <p>Starts a new game with the current settings. Creates the puzzle pieces and distributes them.</p>
369
+ <div class="example-box">
370
+ <strong>Example:</strong>
371
+ <pre><code>puzzle.start();</code></pre>
372
+ </div>
373
+ <div class="warning-box">
374
+ <strong>Important:</strong> Call this only after the puzzle is ready (use the <code>onReady</code> callback).
375
+ </div>
376
+ </div>
377
+
378
+ <div class="method-section">
379
+ <h3><code>stop()</code></h3>
380
+ <p>Stops the current game and returns to the image preview state.</p>
381
+ <pre><code>puzzle.stop();</code></pre>
382
+ </div>
383
+
384
+ <div class="method-section">
385
+ <h3><code>reset()</code></h3>
386
+ <p>Resets the puzzle to initial state. Reloads the current image and prepares for a new game. Use this to start a new game with the same instance.</p>
387
+ <pre><code>puzzle.reset();
388
+ // Then wait for onReady callback and call puzzle.start()</code></pre>
389
+ </div>
390
+
391
+ <div class="method-section">
392
+ <h3><code>setImage(imageUrl)</code></h3>
393
+ <p>Sets a new image for the puzzle.</p>
394
+ <p><strong>Parameters:</strong></p>
395
+ <ul>
396
+ <li><code>imageUrl</code> (string) - URL or data URL of the image</li>
397
+ </ul>
398
+ <div class="example-box">
399
+ <strong>Example:</strong>
400
+ <pre><code>puzzle.setImage('https://example.com/new-image.jpg');
401
+ // Image will reload, wait for onReady callback</code></pre>
402
+ </div>
403
+ </div>
404
+
405
+ <div class="method-section">
406
+ <h3><code>setOptions(newOptions)</code></h3>
407
+ <p>Updates puzzle options without creating a new instance.</p>
408
+ <p><strong>Parameters:</strong></p>
409
+ <ul>
410
+ <li><code>newOptions</code> (object) - Object with options to update (partial updates allowed)</li>
411
+ </ul>
412
+ <div class="example-box">
413
+ <strong>Example:</strong>
414
+ <pre><code>puzzle.setOptions({
415
+ numPieces: 50,
416
+ allowRotation: true,
417
+ shapeType: 1
418
+ });</code></pre>
419
+ </div>
420
+ </div>
421
+
422
+ <div class="method-section">
423
+ <h3><code>save([callback])</code></h3>
424
+ <p>Saves the current game state. Gets the state data, converts it to a JSON string, then either calls the callback with the string or saves to localStorage.</p>
425
+ <p><strong>Parameters:</strong></p>
426
+ <ul>
427
+ <li><code>callback</code> (function, optional) - Function that receives the saved data as JSON string. If not provided, saves to localStorage automatically.</li>
428
+ </ul>
429
+ <div class="example-box">
430
+ <strong>Examples:</strong>
431
+ <pre><code>// Save to localStorage (default)
432
+ puzzle.save();
433
+
434
+ // Save with custom callback
435
+ puzzle.save((savedData) => {
436
+ // savedData is a JSON string
437
+ localStorage.setItem('myPuzzleSave', savedData);
438
+ // Or send to server, download as file, etc.
439
+ });</code></pre>
440
+ </div>
441
+ </div>
442
+
443
+ <div class="method-section">
444
+ <h3>Loading Saved Games</h3>
445
+ <p>To load a saved game, create a new puzzle instance with the <code>savedData</code> option in the constructor. The <code>load()</code> method has been removed in favor of constructor-based loading.</p>
446
+ <div class="example-box">
447
+ <strong>Examples:</strong>
448
+ <pre><code>// Load from explicit string
449
+ const savedString = localStorage.getItem('myPuzzleSave');
450
+ const puzzle = new JigsawPuzzle('puzzle-container', {
451
+ savedData: savedString,
452
+ onReady: () => puzzle.start()
453
+ });
454
+
455
+ // Load from localStorage automatically (pass empty string)
456
+ const puzzle = new JigsawPuzzle('puzzle-container', {
457
+ savedData: "", // Empty string triggers localStorage lookup
458
+ onReady: () => puzzle.start()
459
+ });
460
+
461
+ // Create new puzzle (no saved data)
462
+ const puzzle = new JigsawPuzzle('puzzle-container', {
463
+ image: 'image.jpg',
464
+ numPieces: 25,
465
+ onReady: () => puzzle.start()
466
+ });</code></pre>
467
+ </div>
468
+ <div class="warning-box">
469
+ <strong>Note:</strong> When loading a saved game, you must create a new instance. Destroy the old instance first if needed.
470
+ </div>
471
+ </div>
472
+
473
+ <div class="method-section">
474
+ <h3><code>destroy()</code></h3>
475
+ <p>Completely destroys the puzzle instance, cleaning up all resources. Use this when you want to remove the puzzle and create a new one in the same container.</p>
476
+ <div class="warning-box">
477
+ <strong>Note:</strong> After calling <code>destroy()</code>, you must create a new instance. Event listeners remain attached to the container, but this is usually fine for creating new instances.
478
+ </div>
479
+ <div class="example-box">
480
+ <strong>Example:</strong>
481
+ <pre><code>puzzle.destroy();
482
+ puzzle = new JigsawPuzzle('puzzle-container', {
483
+ image: 'new-image.jpg',
484
+ numPieces: 30
485
+ });</code></pre>
486
+ </div>
487
+ </div>
488
+
489
+ <h2>6. Starting a Game</h2>
490
+
491
+ <p>Here's the recommended pattern for starting a game:</p>
492
+
493
+ <div class="example-box">
494
+ <strong>Complete Example:</strong>
495
+ <pre><code>import { JigsawPuzzle } from './jigsaw-puzzle-game.js';
496
+
497
+ const puzzle = new JigsawPuzzle('puzzle-container', {
498
+ image: 'https://example.com/image.jpg',
499
+ numPieces: 20,
500
+ shapeType: 0,
501
+ allowRotation: false,
502
+ onReady: () => {
503
+ // Puzzle is ready, start the game
504
+ puzzle.start();
505
+ },
506
+ onWin: () => {
507
+ console.log('Puzzle solved!');
508
+ alert('Congratulations!');
509
+ },
510
+ onStart: () => {
511
+ console.log('Game started');
512
+ },
513
+ onStop: () => {
514
+ console.log('Game stopped');
515
+ },
516
+ onMerged: (piece) => {
517
+ console.log('Pieces merged!', piece);
518
+ },
519
+ onChanged: (piece) => {
520
+ console.log('Piece moved', piece);
521
+ },
522
+ onDeleted: (piece) => {
523
+ console.log('Piece deleted', piece);
524
+ }
525
+ });</code></pre>
526
+ </div>
527
+
528
+ <p><strong>Important:</strong> Always use the <code>onReady</code> callback to start the game. This ensures the image is loaded before starting.</p>
529
+
530
+ <h2>7. Starting a New Game</h2>
531
+
532
+ <p>There are several ways to start a new game:</p>
533
+
534
+ <h3>Option 1: Load Saved Game</h3>
535
+ <p>To load a previously saved game, create a new instance with the <code>savedData</code> option:</p>
536
+ <pre><code>// Load from explicit string
537
+ const savedString = localStorage.getItem('puzzleSave');
538
+ puzzle = new JigsawPuzzle('puzzle-container', {
539
+ savedData: savedString,
540
+ onReady: () => puzzle.start()
541
+ });
542
+
543
+ // Or load from localStorage automatically
544
+ puzzle = new JigsawPuzzle('puzzle-container', {
545
+ savedData: "", // Empty string triggers localStorage lookup
546
+ onReady: () => puzzle.start()
547
+ });</code></pre>
548
+
549
+ <h3>Option 2: Reset and Reuse (Same Instance)</h3>
550
+ <p>If you want to restart with the same image and settings:</p>
551
+ <pre><code>puzzle.reset();
552
+ // The puzzle will reload the image
553
+ // Wait for onReady callback, then:
554
+ puzzle.start();</code></pre>
555
+
556
+ <h3>Option 3: Change Image and Reset</h3>
557
+ <p>If you want to change the image:</p>
558
+ <pre><code>puzzle.setImage('new-image.jpg');
559
+ puzzle.reset();
560
+ // Wait for onReady callback, then:
561
+ puzzle.start();</code></pre>
562
+
563
+ <h3>Option 4: Change Settings, Image, and Reset</h3>
564
+ <p>If you want to change multiple settings:</p>
565
+ <pre><code>puzzle.setImage('new-image.jpg');
566
+ puzzle.setOptions({
567
+ numPieces: 50,
568
+ shapeType: 1,
569
+ allowRotation: true
570
+ });
571
+ puzzle.reset();
572
+ // Wait for onReady callback, then:
573
+ puzzle.start();</code></pre>
574
+
575
+ <h3>Option 5: Destroy and Create New (Clean Slate)</h3>
576
+ <p>For a completely fresh start with potentially different options:</p>
577
+ <pre><code>puzzle.destroy();
578
+ puzzle = new JigsawPuzzle('puzzle-container', {
579
+ image: 'completely-different-image.jpg',
580
+ numPieces: 30,
581
+ shapeType: 2,
582
+ allowRotation: true,
583
+ onReady: () => {
584
+ puzzle.start();
585
+ }
586
+ });</code></pre>
587
+
588
+ <h2>8. User Interactions</h2>
589
+
590
+ <p>The puzzle supports various user interactions:</p>
591
+ <ul>
592
+ <li><strong>Mouse/Touch:</strong> Click and drag pieces to move them</li>
593
+ <li><strong>Rotation:</strong> If <code>allowRotation</code> is enabled, quick click/tap rotates pieces 90°</li>
594
+ <li><strong>Piece Merging:</strong> When pieces are close and correctly aligned, they automatically merge</li>
595
+ <li><strong>Pan:</strong> Click and drag on empty space to pan all pieces</li>
596
+ <li><strong>Zoom:</strong> Mouse wheel to zoom in/out, or pinch gesture on touch devices</li>
597
+ <li><strong>Two-finger Zoom:</strong> Use two fingers on touch devices to zoom</li>
598
+ </ul>
599
+
600
+ <h2>9. Styling</h2>
601
+
602
+ <p>The puzzle container should have a defined size. Full-screen example:</p>
603
+ <pre><code>#puzzle-container {
604
+ width: 100vw;
605
+ height: 100vh;
606
+ position: relative;
607
+ }</code></pre>
608
+
609
+ <p>The puzzle adds these CSS classes you can style:</p>
610
+ <ul>
611
+ <li><code>.polypiece</code> - Individual puzzle pieces</li>
612
+ <li><code>.polypiece.moving</code> - Pieces during animation</li>
613
+ <li><code>.gameCanvas</code> - Reference image canvas (hidden during play)</li>
614
+ </ul>
615
+
616
+ <h2>10. Browser Compatibility</h2>
617
+
618
+ <p>The puzzle requires modern browser features:</p>
619
+ <ul>
620
+ <li>ES6 Modules support</li>
621
+ <li>Canvas API</li>
622
+ <li>Path2D API</li>
623
+ <li>Touch events (for mobile support)</li>
624
+ </ul>
625
+
626
+ <p>Works in all modern browsers (Chrome, Firefox, Safari, Edge).</p>
627
+
628
+ <h2>11. Common Patterns</h2>
629
+
630
+ <h3>Complete Game with Save/Load</h3>
631
+ <pre><code>let puzzle;
632
+
633
+ function startNewGame(imageUrl, numPieces) {
634
+ if (puzzle) puzzle.destroy();
635
+
636
+ puzzle = new JigsawPuzzle('puzzle-container', {
637
+ image: imageUrl,
638
+ numPieces: numPieces,
639
+ allowRotation: false,
640
+ onReady: () => puzzle.start(),
641
+ onWin: () => {
642
+ alert('You won!');
643
+ // Optionally save completion, show next puzzle, etc.
644
+ }
645
+ });
646
+ }
647
+
648
+ // Save game
649
+ function saveGame() {
650
+ puzzle.save((data) => {
651
+ localStorage.setItem('puzzleSave', data);
652
+ console.log('Game saved!');
653
+ });
654
+ }
655
+
656
+ // Load game - create new instance with saved data
657
+ function loadGame() {
658
+ const saved = localStorage.getItem('puzzleSave');
659
+ if (saved) {
660
+ if (puzzle) puzzle.destroy();
661
+ puzzle = new JigsawPuzzle('puzzle-container', {
662
+ savedData: saved,
663
+ onReady: () => puzzle.start(),
664
+ onWin: () => {
665
+ alert('You won!');
666
+ }
667
+ });
668
+ }
669
+ }</code></pre>
670
+
671
+ <h2>12. Troubleshooting</h2>
672
+
673
+ <div class="warning-box">
674
+ <h3>Puzzle doesn't start</h3>
675
+ <ul>
676
+ <li>Make sure you're calling <code>start()</code> in the <code>onReady</code> callback</li>
677
+ <li>Check that the image URL is valid and accessible (CORS issues if loading from different domain)</li>
678
+ <li>Verify the container element exists and has a size</li>
679
+ </ul>
680
+ </div>
681
+
682
+ <div class="warning-box">
683
+ <h3>Image doesn't load</h3>
684
+ <ul>
685
+ <li>Check browser console for CORS errors</li>
686
+ <li>Use data URLs or images from the same domain</li>
687
+ <li>Ensure the image URL is correct</li>
688
+ </ul>
689
+ </div>
690
+
691
+ <div class="warning-box">
692
+ <h3>Pieces don't merge</h3>
693
+ <ul>
694
+ <li>Make sure pieces are close enough (the puzzle calculates optimal distance)</li>
695
+ <li>Check that pieces are in the same rotation if rotation is enabled</li>
696
+ <li>Verify pieces are correctly aligned</li>
697
+ </ul>
698
+ </div>
699
+
700
+ <hr>
701
+ <p style="text-align: center; color: #888; margin-top: 40px;">
702
+ JigsawPuzzle Class Documentation | Version 1.0.8
703
+ </p>
704
+
705
+ <hr style="margin: 40px 0;">
706
+ <div style="background-color: #f9f9f9; padding: 20px; border-radius: 5px; margin: 40px 0;">
707
+ <h2 style="margin-top: 0;">License</h2>
708
+ <pre style="background-color: #f4f4f4; color: #333; padding: 15px; border-radius: 5px; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word; font-family: 'Courier New', monospace; line-height: 1.6;">Copyright (c) 2026 by Dillon (https://codepen.io/Dillo/pen/QWKLYab) & Henrik Rasmussen (https://www.raketten.net)
709
+
710
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
711
+
712
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
713
+
714
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</pre>
715
+ </div>
716
+ </body>
717
+ </html>
718
+
@@ -381,8 +381,10 @@ class Piece {
381
381
  // ============================================================================
382
382
 
383
383
  class PolyPiece {
384
- constructor(initialPiece, puzzleInstance) {
384
+ constructor(initialPiece, puzzleInstance, yIndex, xIndex) {
385
385
  this.puzzle = puzzleInstance;
386
+ this.yIndex = yIndex;
387
+ this.xIndex = xIndex;
386
388
  this.pckxmin = initialPiece.kx;
387
389
  this.pckxmax = initialPiece.kx + 1;
388
390
  this.pckymin = initialPiece.ky;
@@ -414,7 +416,7 @@ class PolyPiece {
414
416
  const kOther = puzzle.polyPieces.indexOf(otherPoly);
415
417
  puzzle.polyPieces.splice(kOther, 1);
416
418
  puzzle.container.removeChild(otherPoly.canvas);
417
-
419
+
418
420
  for (let k = 0; k < otherPoly.pieces.length; ++k) {
419
421
  this.pieces.push(otherPoly.pieces[k]);
420
422
  if (otherPoly.pieces[k].kx < this.pckxmin)
@@ -663,8 +665,8 @@ class PolyPiece {
663
665
  : "gold"
664
666
  : "rgba(0, 0, 0, 0.5)";
665
667
  this.ctx.shadowBlur = this.selected ? mmin(8, puzzle.scalex / 10) : 4;
666
- this.ctx.shadowOffsetX = this.selected ? 0 : -4;
667
- this.ctx.shadowOffsetY = this.selected ? 0 : 4;
668
+ this.ctx.shadowOffsetX = this.selected ? 0 : ([-4, 4, 4, -4][this.rot]);
669
+ this.ctx.shadowOffsetY = this.selected ? 0 : ([4, 4, -4, -4][this.rot]);
668
670
  this.ctx.fill(this.path);
669
671
  if (this.selected) {
670
672
  for (let i = 0; i < 6; i++) this.ctx.fill(this.path);
@@ -703,25 +705,19 @@ class PolyPiece {
703
705
 
704
706
  this.ctx.drawImage(
705
707
  puzzle.gameCanvas,
706
- srcx,
707
- srcy,
708
- w,
709
- h,
710
- destx,
711
- desty,
712
- w,
713
- h
708
+ srcx, srcy, w, h,
709
+ destx, desty, w, h
714
710
  );
715
711
  this.ctx.lineWidth = puzzle.embossThickness * 1.5;
716
712
 
717
- this.ctx.translate(
718
- puzzle.embossThickness / 2,
719
- -puzzle.embossThickness / 2
720
- );
713
+ const dxemboss = puzzle.embossThickness / 2 * [1, -1, -1, 1][this.rot];
714
+ const dyemboss = puzzle.embossThickness / 2 * [-1, -1, 1, 1][this.rot];
715
+
716
+ this.ctx.translate(dxemboss, dyemboss);
721
717
  this.ctx.strokeStyle = "rgba(0, 0, 0, 0.35)";
722
718
  this.ctx.stroke(path);
723
719
 
724
- this.ctx.translate(-puzzle.embossThickness, puzzle.embossThickness);
720
+ this.ctx.translate(-2 * dxemboss, -2 * dyemboss);
725
721
  this.ctx.strokeStyle = "rgba(255, 255, 255, 0.35)";
726
722
  this.ctx.stroke(path);
727
723
 
@@ -911,9 +907,10 @@ class InternalPuzzle {
911
907
 
912
908
  this.polyPieces = [];
913
909
  if (!baseData) {
914
- this.pieces.forEach((row) =>
915
- row.forEach((piece) => {
916
- this.polyPieces.push(new PolyPiece(piece, this));
910
+ this.pieces.forEach((row,yIndex) =>
911
+ row.forEach((piece,xIndex) => {
912
+ console.log('Piece:', yIndex, xIndex);
913
+ this.polyPieces.push(new PolyPiece(piece, this, yIndex, xIndex));
917
914
  })
918
915
  );
919
916
  arrayShuffle(this.polyPieces);
@@ -926,6 +923,7 @@ class InternalPuzzle {
926
923
  const pps = baseData[8];
927
924
  const offs = this.rotationAllowed ? 3 : 2;
928
925
  pps.forEach((ppData) => {
926
+ console.log('ppData:', ppData);
929
927
  let polyp = new PolyPiece(this.pieces[ppData[offs + 1]][ppData[offs]], this);
930
928
  polyp.x = ppData[0];
931
929
  polyp.y = ppData[1];
@@ -1362,8 +1360,12 @@ export class JigsawPuzzle {
1362
1360
  onReady: options.onReady || null,
1363
1361
  onWin: options.onWin || null,
1364
1362
  onStart: options.onStart || null,
1365
- onStop: options.onStop || null
1363
+ onStop: options.onStop || null,
1364
+ onMerged: options.onMerged || null,
1365
+ onChanged: options.onChanged || null,
1366
+ onDeleted: options.onDeleted || null,
1366
1367
  };
1368
+ console.log('Options2:', this.options);
1367
1369
 
1368
1370
  // Create internal puzzle instance
1369
1371
  this.puzzle = new InternalPuzzle(container);
@@ -1631,6 +1633,7 @@ export class JigsawPuzzle {
1631
1633
  } else return;
1632
1634
 
1633
1635
  case 20:
1636
+ console.log('state 20');
1634
1637
  this.playing = true;
1635
1638
  if (this.options.onStart) this.options.onStart();
1636
1639
  this.puzzle.rotationAllowed = this.options.allowRotation;
@@ -1663,6 +1666,7 @@ export class JigsawPuzzle {
1663
1666
  break;
1664
1667
 
1665
1668
  case 25:
1669
+ console.log('state 25');
1666
1670
  this.puzzle.gameCanvas.style.display = "none";
1667
1671
  this.puzzle.polyPieces.forEach((pp) => {
1668
1672
  pp.canvas.classList.add("moving");
@@ -1671,12 +1675,14 @@ export class JigsawPuzzle {
1671
1675
  break;
1672
1676
 
1673
1677
  case 30:
1678
+ console.log('state 30');
1674
1679
  this.puzzle.optimInitial();
1675
1680
  setTimeout(() => this.events.push({ event: "finished" }), 1200);
1676
1681
  this.state = 35;
1677
1682
  break;
1678
1683
 
1679
1684
  case 35:
1685
+ console.log('state 35');
1680
1686
  if (!event || event.event !== "finished") return;
1681
1687
  this.puzzle.polyPieces.forEach((pp) => {
1682
1688
  pp.canvas.classList.remove("moving");
@@ -1686,6 +1692,7 @@ export class JigsawPuzzle {
1686
1692
 
1687
1693
  case 50:
1688
1694
  if (!event) return;
1695
+ console.log('state 50',event.event);
1689
1696
  if (event.event === "stop") {
1690
1697
  this.state = 10;
1691
1698
  return;
@@ -1726,6 +1733,7 @@ export class JigsawPuzzle {
1726
1733
 
1727
1734
  case 55:
1728
1735
  if (!event) return;
1736
+ console.log('state 55-123',event.event);
1729
1737
  if (event.event === "stop") {
1730
1738
  this.state = 10;
1731
1739
  return;
@@ -1765,10 +1773,14 @@ export class JigsawPuzzle {
1765
1773
  if (this.moving.pp.ifNear(pp)) {
1766
1774
  merged = true;
1767
1775
  if (pp.pieces.length > this.moving.pp.pieces.length) {
1776
+ if (this.options.onDeleted) this.options.onDeleted(this.moving.pp);
1768
1777
  pp.merge(this.moving.pp);
1778
+ if (this.options.onMerged) this.options.onMerged(pp);
1769
1779
  this.moving.pp = pp;
1770
- } else {
1780
+ } else {
1781
+ if (this.options.onDeleted) this.options.onDeleted(pp);
1771
1782
  this.moving.pp.merge(pp);
1783
+ if (this.options.onMerged) this.options.onMerged(this.moving.pp);
1772
1784
  }
1773
1785
  doneSomething = true;
1774
1786
  break;
@@ -1780,13 +1792,14 @@ export class JigsawPuzzle {
1780
1792
  this.moving.pp.selected = true;
1781
1793
  this.moving.pp.drawImage(true);
1782
1794
  this.moving.tInit = tStamp + 500;
1783
- this.state = 56;
1795
+ this.state = 56;
1784
1796
  break;
1785
1797
  }
1786
1798
  this.state = 50;
1787
1799
  if (this.puzzle.polyPieces.length === 1 && this.puzzle.polyPieces[0].rot === 0) {
1788
1800
  this.state = 60;
1789
1801
  }
1802
+ if (this.options.onChanged) this.options.onChanged(this.moving.pp);
1790
1803
  }
1791
1804
  break;
1792
1805
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jigsawpuzzlegame",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "A class for creating a jigsaw puzzle",
5
5
  "keywords": [
6
6
  "jigsaw",