PierreH commited on
Commit
0771ac6
·
verified ·
1 Parent(s): 8a81557

Add 1 files

Browse files
Files changed (1) hide show
  1. index.html +554 -19
index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Étoile de Mots Interactive</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
  <style>
@@ -22,6 +22,7 @@
22
  border-radius: 50%;
23
  z-index: 1;
24
  opacity: 0.7;
 
25
  }
26
 
27
  .circle1 { background-color: #fecaca; width: 8%; height: 8%; top: 46%; left: 46%; }
@@ -61,6 +62,12 @@
61
  z-index: 10;
62
  }
63
 
 
 
 
 
 
 
64
  #canvas {
65
  position: absolute;
66
  top: 0;
@@ -183,6 +190,62 @@
183
  max-width: 400px;
184
  }
185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  @media (max-width: 640px) {
187
  .label {
188
  font-size: 0.75rem;
@@ -212,9 +275,13 @@
212
  <div class="w-full max-w-2xl bg-white rounded-xl shadow-lg p-6 space-y-6">
213
  <div class="flex justify-between items-center">
214
  <h1 class="text-2xl font-bold text-center text-gray-800">
215
- <i class="fas fa-star text-yellow-400 mr-2"></i>Étoile de Mots
216
  </h1>
217
  <div class="flex gap-2">
 
 
 
 
218
  <button onclick="resetKeywords()" class="action-btn bg-gray-500 text-white px-4 py-2 rounded-lg hover:bg-gray-600">
219
  <i class="fas fa-redo"></i>
220
  <span class="mobile-hidden">Réinit.</span>
@@ -222,16 +289,53 @@
222
  </div>
223
  </div>
224
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  <textarea id="userNotes" class="note-input w-full"
226
  placeholder="Titre, notes ou objectifs... ✍️" rows="2"></textarea>
227
 
228
  <div class="target-container shadow-inner" id="target-container">
 
229
  <canvas id="canvas"></canvas>
230
- <div class="circle circle5"></div>
231
- <div class="circle circle4"></div>
232
- <div class="circle circle3"></div>
233
- <div class="circle circle2"></div>
234
- <div class="circle circle1"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  </div>
236
 
237
  <div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
@@ -242,6 +346,12 @@
242
  <i class="fas fa-plus"></i>
243
  <span>Ajouter</span>
244
  </button>
 
 
 
 
 
 
245
  </div>
246
 
247
  <div class="context-menu" id="contextMenu">
@@ -254,6 +364,9 @@
254
  <div class="context-menu-item" onclick="showStyleEditor()">
255
  <i class="fas fa-paint-brush text-purple-500"></i> Style
256
  </div>
 
 
 
257
  <div class="context-menu-item" onclick="deselectKeyword()">
258
  <i class="fas fa-times-circle text-gray-500"></i> Désélectionner
259
  </div>
@@ -289,6 +402,7 @@
289
  <option value="0.75rem">Petit</option>
290
  <option value="0.85rem" selected>Moyen</option>
291
  <option value="1rem">Grand</option>
 
292
  </select>
293
  </div>
294
 
@@ -298,6 +412,17 @@
298
  <option value="400">Normal</option>
299
  <option value="500" selected>Medium</option>
300
  <option value="600">Gras</option>
 
 
 
 
 
 
 
 
 
 
 
301
  </select>
302
  </div>
303
 
@@ -309,6 +434,7 @@
309
  <div class="color-option bg-green-500" data-color="#10b981" onclick="selectLineColor(this)"></div>
310
  <div class="color-option bg-purple-500" data-color="#8b5cf6" onclick="selectLineColor(this)"></div>
311
  <div class="color-option bg-pink-500" data-color="#ec4899" onclick="selectLineColor(this)"></div>
 
312
  </div>
313
 
314
  <div class="mt-3">
@@ -330,20 +456,62 @@
330
  Appliquer
331
  </button>
332
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  </div>
334
 
335
  <script>
336
  let keywords = [
337
- { text: "Identification", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500" },
338
- { text: "Gestion", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500" },
339
- { text: "Adaptation", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500" },
340
- { text: "Autonomie", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500" },
341
- { text: "Connaissance", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500" },
342
- { text: "Prévention", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500" },
343
- { text: "Équilibre", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500" },
344
- { text: "Stratégies", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500" },
345
- { text: "Motivation", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500" },
346
- { text: "Recours", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500" }
 
 
 
 
 
 
347
  ];
348
 
349
  let selectedKeywordIndex = null;
@@ -354,11 +522,17 @@
354
  let lineColor = "#3b82f6";
355
  let lineWidth = 1;
356
  let lineStyle = "solid";
 
 
 
 
 
357
 
358
  const canvas = document.getElementById('canvas');
359
  const ctx = canvas.getContext('2d');
360
  const contextMenu = document.getElementById('contextMenu');
361
  const editModal = document.getElementById('editModal');
 
362
 
363
  function renderKeywords() {
364
  const container = document.getElementById('target-container');
@@ -369,11 +543,24 @@
369
  // Remove existing labels
370
  container.querySelectorAll('.label').forEach(e => e.remove());
371
 
 
 
 
372
  // Create new labels
373
  keywords.forEach((word, index) => {
 
 
 
374
  const label = document.createElement('div');
375
  label.className = 'label draggable';
376
- label.textContent = word.text;
 
 
 
 
 
 
 
377
  label.dataset.index = index;
378
 
379
  // Apply custom styles
@@ -381,6 +568,7 @@
381
  label.style.backgroundColor = word.bgColor;
382
  label.style.fontSize = word.fontSize;
383
  label.style.fontWeight = word.fontWeight;
 
384
 
385
  // Calculate position if not set
386
  if (word.x === null || word.y === null) {
@@ -423,6 +611,7 @@
423
  setTimeout(() => {
424
  updateLabelPositions();
425
  resizeCanvas();
 
426
  }, 0);
427
  }
428
 
@@ -453,6 +642,7 @@
453
  isDragging = true;
454
  draggedLabel = label;
455
  draggedIndex = index;
 
456
 
457
  const rect = label.getBoundingClientRect();
458
  dragStartX = e.clientX - rect.left;
@@ -479,6 +669,29 @@
479
  let newX = e.clientX - containerRect.left - dragStartX;
480
  let newY = e.clientY - containerRect.top - dragStartY;
481
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
  // Constrain to container bounds
483
  newX = Math.max(0, Math.min(newX, container.offsetWidth - draggedLabel.offsetWidth));
484
  newY = Math.max(0, Math.min(newY, container.offsetHeight - draggedLabel.offsetHeight));
@@ -505,6 +718,10 @@
505
  }
506
 
507
  function stopDrag() {
 
 
 
 
508
  isDragging = false;
509
  draggedLabel = null;
510
  draggedIndex = null;
@@ -608,7 +825,9 @@
608
  color: "#1e293b",
609
  bgColor: "#ffffff",
610
  fontSize: "0.85rem",
611
- fontWeight: "500"
 
 
612
  });
613
  input.value = '';
614
  renderKeywords();
@@ -693,6 +912,7 @@
693
  document.getElementById('bgColor').value = keyword.bgColor;
694
  document.getElementById('fontSize').value = keyword.fontSize;
695
  document.getElementById('fontWeight').value = keyword.fontWeight;
 
696
 
697
  editor.style.display = 'block';
698
  contextMenu.style.display = 'none';
@@ -717,6 +937,7 @@
717
  keyword.bgColor = document.getElementById('bgColor').value;
718
  keyword.fontSize = document.getElementById('fontSize').value;
719
  keyword.fontWeight = document.getElementById('fontWeight').value;
 
720
 
721
  // Update line styles
722
  lineWidth = document.getElementById('lineWidth').value;
@@ -726,6 +947,271 @@
726
  document.getElementById('styleEditor').style.display = 'none';
727
  }
728
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
729
  // Initialize
730
  document.addEventListener('DOMContentLoaded', () => {
731
  renderKeywords();
@@ -753,6 +1239,55 @@
753
  if (editModal.style.display === 'flex' && !editModal.querySelector('.edit-modal-content').contains(e.target)) {
754
  closeEditModal();
755
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
756
  });
757
  });
758
  </script>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Étoile de Mots Interactive Pro</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
  <style>
 
22
  border-radius: 50%;
23
  z-index: 1;
24
  opacity: 0.7;
25
+ transition: all 0.5s ease;
26
  }
27
 
28
  .circle1 { background-color: #fecaca; width: 8%; height: 8%; top: 46%; left: 46%; }
 
62
  z-index: 10;
63
  }
64
 
65
+ .label.dragging {
66
+ box-shadow: 0 0 15px rgba(0,0,0,0.2);
67
+ z-index: 20;
68
+ opacity: 0.9;
69
+ }
70
+
71
  #canvas {
72
  position: absolute;
73
  top: 0;
 
190
  max-width: 400px;
191
  }
192
 
193
+ .grid-lines {
194
+ position: absolute;
195
+ top: 0;
196
+ left: 0;
197
+ width: 100%;
198
+ height: 100%;
199
+ z-index: 2;
200
+ pointer-events: none;
201
+ background-image:
202
+ linear-gradient(to right, rgba(0,0,0,0.05) 1px, transparent 1px),
203
+ linear-gradient(to bottom, rgba(0,0,0,0.05) 1px, transparent 1px);
204
+ background-size: 20px 20px;
205
+ }
206
+
207
+ .snap-point {
208
+ position: absolute;
209
+ width: 6px;
210
+ height: 6px;
211
+ background-color: rgba(59, 130, 246, 0.5);
212
+ border-radius: 50%;
213
+ z-index: 3;
214
+ pointer-events: none;
215
+ }
216
+
217
+ .pulse {
218
+ animation: pulse 2s infinite;
219
+ }
220
+
221
+ @keyframes pulse {
222
+ 0% { transform: scale(1); opacity: 0.7; }
223
+ 50% { transform: scale(1.02); opacity: 0.9; }
224
+ 100% { transform: scale(1); opacity: 0.7; }
225
+ }
226
+
227
+ .layer-selector {
228
+ position: absolute;
229
+ top: 10px;
230
+ right: 10px;
231
+ z-index: 50;
232
+ background: white;
233
+ padding: 0.5rem;
234
+ border-radius: 0.5rem;
235
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
236
+ }
237
+
238
+ .theme-group {
239
+ position: absolute;
240
+ top: 10px;
241
+ left: 10px;
242
+ z-index: 50;
243
+ background: white;
244
+ padding: 0.5rem;
245
+ border-radius: 0.5rem;
246
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
247
+ }
248
+
249
  @media (max-width: 640px) {
250
  .label {
251
  font-size: 0.75rem;
 
275
  <div class="w-full max-w-2xl bg-white rounded-xl shadow-lg p-6 space-y-6">
276
  <div class="flex justify-between items-center">
277
  <h1 class="text-2xl font-bold text-center text-gray-800">
278
+ <i class="fas fa-star text-yellow-400 mr-2"></i>Étoile de Mots Pro
279
  </h1>
280
  <div class="flex gap-2">
281
+ <button onclick="exportToPNG()" class="action-btn bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600">
282
+ <i class="fas fa-download"></i>
283
+ <span class="mobile-hidden">Exporter</span>
284
+ </button>
285
  <button onclick="resetKeywords()" class="action-btn bg-gray-500 text-white px-4 py-2 rounded-lg hover:bg-gray-600">
286
  <i class="fas fa-redo"></i>
287
  <span class="mobile-hidden">Réinit.</span>
 
289
  </div>
290
  </div>
291
 
292
+ <div class="flex gap-2">
293
+ <button onclick="exportToJSON()" class="action-btn bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600">
294
+ <i class="fas fa-save"></i>
295
+ <span class="mobile-hidden">Sauvegarder</span>
296
+ </button>
297
+ <button onclick="importFromJSON()" class="action-btn bg-purple-500 text-white px-4 py-2 rounded-lg hover:bg-purple-600">
298
+ <i class="fas fa-folder-open"></i>
299
+ <span class="mobile-hidden">Charger</span>
300
+ </button>
301
+ <button onclick="toggleGrid()" id="gridToggle" class="action-btn bg-yellow-500 text-white px-4 py-2 rounded-lg hover:bg-yellow-600">
302
+ <i class="fas fa-th"></i>
303
+ <span class="mobile-hidden">Grille</span>
304
+ </button>
305
+ <button onclick="toggleSnap()" id="snapToggle" class="action-btn bg-pink-500 text-white px-4 py-2 rounded-lg hover:bg-pink-600">
306
+ <i class="fas fa-magnet"></i>
307
+ <span class="mobile-hidden">Magnétisme</span>
308
+ </button>
309
+ </div>
310
+
311
  <textarea id="userNotes" class="note-input w-full"
312
  placeholder="Titre, notes ou objectifs... ✍️" rows="2"></textarea>
313
 
314
  <div class="target-container shadow-inner" id="target-container">
315
+ <div class="grid-lines" id="gridLines" style="display: none;"></div>
316
  <canvas id="canvas"></canvas>
317
+ <div class="circle circle5 pulse"></div>
318
+ <div class="circle circle4 pulse"></div>
319
+ <div class="circle circle3 pulse"></div>
320
+ <div class="circle circle2 pulse"></div>
321
+ <div class="circle circle1 pulse"></div>
322
+
323
+ <div class="layer-selector">
324
+ <select id="layerSelect" class="border rounded p-1 text-sm" onchange="changeLayer()">
325
+ <option value="0">Couche 1</option>
326
+ <option value="1">Couche 2</option>
327
+ <option value="2">Couche 3</option>
328
+ </select>
329
+ </div>
330
+
331
+ <div class="theme-group">
332
+ <select id="themeSelect" class="border rounded p-1 text-sm" onchange="groupByTheme()">
333
+ <option value="">Tous les thèmes</option>
334
+ <option value="1">Thème 1</option>
335
+ <option value="2">Thème 2</option>
336
+ <option value="3">Thème 3</option>
337
+ </select>
338
+ </div>
339
  </div>
340
 
341
  <div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
 
346
  <i class="fas fa-plus"></i>
347
  <span>Ajouter</span>
348
  </button>
349
+
350
+ <select id="fontSelect" class="border rounded p-2">
351
+ <option value="'Segoe UI', Roboto, sans-serif">Sans-serif</option>
352
+ <option value="Georgia, 'Times New Roman', serif">Serif</option>
353
+ <option value="'Comic Sans MS', cursive">Manuscrite</option>
354
+ </select>
355
  </div>
356
 
357
  <div class="context-menu" id="contextMenu">
 
364
  <div class="context-menu-item" onclick="showStyleEditor()">
365
  <i class="fas fa-paint-brush text-purple-500"></i> Style
366
  </div>
367
+ <div class="context-menu-item" onclick="addIconToKeyword()">
368
+ <i class="fas fa-icons text-yellow-500"></i> Ajouter icône
369
+ </div>
370
  <div class="context-menu-item" onclick="deselectKeyword()">
371
  <i class="fas fa-times-circle text-gray-500"></i> Désélectionner
372
  </div>
 
402
  <option value="0.75rem">Petit</option>
403
  <option value="0.85rem" selected>Moyen</option>
404
  <option value="1rem">Grand</option>
405
+ <option value="1.2rem">Très grand</option>
406
  </select>
407
  </div>
408
 
 
412
  <option value="400">Normal</option>
413
  <option value="500" selected>Medium</option>
414
  <option value="600">Gras</option>
415
+ <option value="700">Très gras</option>
416
+ </select>
417
+ </div>
418
+
419
+ <div class="style-option">
420
+ <span>Thème:</span>
421
+ <select id="keywordTheme" class="border rounded p-1">
422
+ <option value="0">Aucun</option>
423
+ <option value="1">Thème 1</option>
424
+ <option value="2">Thème 2</option>
425
+ <option value="3">Thème 3</option>
426
  </select>
427
  </div>
428
 
 
434
  <div class="color-option bg-green-500" data-color="#10b981" onclick="selectLineColor(this)"></div>
435
  <div class="color-option bg-purple-500" data-color="#8b5cf6" onclick="selectLineColor(this)"></div>
436
  <div class="color-option bg-pink-500" data-color="#ec4899" onclick="selectLineColor(this)"></div>
437
+ <div class="color-option bg-gray-900" data-color="#111827" onclick="selectLineColor(this)"></div>
438
  </div>
439
 
440
  <div class="mt-3">
 
456
  Appliquer
457
  </button>
458
  </div>
459
+
460
+ <div class="edit-modal" id="iconModal" style="display: none;">
461
+ <div class="edit-modal-content">
462
+ <h3 class="font-bold text-lg mb-4">Ajouter une icône</h3>
463
+ <div class="grid grid-cols-4 gap-2 mb-4">
464
+ <div class="icon-option p-2 text-center rounded hover:bg-gray-100 cursor-pointer" onclick="selectIcon('fa-heart')">
465
+ <i class="fas fa-heart text-red-500 text-xl"></i>
466
+ </div>
467
+ <div class="icon-option p-2 text-center rounded hover:bg-gray-100 cursor-pointer" onclick="selectIcon('fa-lightbulb')">
468
+ <i class="fas fa-lightbulb text-yellow-500 text-xl"></i>
469
+ </div>
470
+ <div class="icon-option p-2 text-center rounded hover:bg-gray-100 cursor-pointer" onclick="selectIcon('fa-flag')">
471
+ <i class="fas fa-flag text-blue-500 text-xl"></i>
472
+ </div>
473
+ <div class="icon-option p-2 text-center rounded hover:bg-gray-100 cursor-pointer" onclick="selectIcon('fa-star')">
474
+ <i class="fas fa-star text-purple-500 text-xl"></i>
475
+ </div>
476
+ <div class="icon-option p-2 text-center rounded hover:bg-gray-100 cursor-pointer" onclick="selectIcon('fa-check')">
477
+ <i class="fas fa-check text-green-500 text-xl"></i>
478
+ </div>
479
+ <div class="icon-option p-2 text-center rounded hover:bg-gray-100 cursor-pointer" onclick="selectIcon('fa-exclamation')">
480
+ <i class="fas fa-exclamation text-orange-500 text-xl"></i>
481
+ </div>
482
+ <div class="icon-option p-2 text-center rounded hover:bg-gray-100 cursor-pointer" onclick="selectIcon('fa-question')">
483
+ <i class="fas fa-question text-indigo-500 text-xl"></i>
484
+ </div>
485
+ <div class="icon-option p-2 text-center rounded hover:bg-gray-100 cursor-pointer" onclick="selectIcon('fa-bolt')">
486
+ <i class="fas fa-bolt text-yellow-500 text-xl"></i>
487
+ </div>
488
+ </div>
489
+ <div class="flex justify-end gap-2">
490
+ <button onclick="closeIconModal()" class="px-4 py-2 rounded-lg border">Annuler</button>
491
+ <button onclick="applyIcon()" class="px-4 py-2 rounded-lg bg-blue-500 text-white">Appliquer</button>
492
+ </div>
493
+ </div>
494
+ </div>
495
  </div>
496
 
497
  <script>
498
  let keywords = [
499
+ { text: "Identification", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500", theme: 1, icon: "" },
500
+ { text: "Gestion", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500", theme: 1, icon: "" },
501
+ { text: "Adaptation", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500", theme: 2, icon: "" },
502
+ { text: "Autonomie", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500", theme: 2, icon: "" },
503
+ { text: "Connaissance", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500", theme: 3, icon: "" },
504
+ { text: "Prévention", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500", theme: 3, icon: "" },
505
+ { text: "Équilibre", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500", theme: 1, icon: "" },
506
+ { text: "Stratégies", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500", theme: 2, icon: "" },
507
+ { text: "Motivation", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500", theme: 3, icon: "" },
508
+ { text: "Recours", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500", theme: 1, icon: "" }
509
+ ];
510
+
511
+ let layers = [
512
+ JSON.parse(JSON.stringify(keywords)),
513
+ JSON.parse(JSON.stringify(keywords)),
514
+ JSON.parse(JSON.stringify(keywords))
515
  ];
516
 
517
  let selectedKeywordIndex = null;
 
522
  let lineColor = "#3b82f6";
523
  let lineWidth = 1;
524
  let lineStyle = "solid";
525
+ let currentLayer = 0;
526
+ let gridEnabled = false;
527
+ let snapEnabled = false;
528
+ let selectedIcon = "";
529
+ let selectedFont = "'Segoe UI', Roboto, sans-serif";
530
 
531
  const canvas = document.getElementById('canvas');
532
  const ctx = canvas.getContext('2d');
533
  const contextMenu = document.getElementById('contextMenu');
534
  const editModal = document.getElementById('editModal');
535
+ const iconModal = document.getElementById('iconModal');
536
 
537
  function renderKeywords() {
538
  const container = document.getElementById('target-container');
 
543
  // Remove existing labels
544
  container.querySelectorAll('.label').forEach(e => e.remove());
545
 
546
+ // Get current theme filter
547
+ const themeFilter = document.getElementById('themeSelect').value;
548
+
549
  // Create new labels
550
  keywords.forEach((word, index) => {
551
+ // Skip if theme filter is active and doesn't match
552
+ if (themeFilter && word.theme != themeFilter) return;
553
+
554
  const label = document.createElement('div');
555
  label.className = 'label draggable';
556
+
557
+ // Add icon if exists
558
+ if (word.icon) {
559
+ label.innerHTML = `<i class="fas ${word.icon} mr-1"></i>${word.text}`;
560
+ } else {
561
+ label.textContent = word.text;
562
+ }
563
+
564
  label.dataset.index = index;
565
 
566
  // Apply custom styles
 
568
  label.style.backgroundColor = word.bgColor;
569
  label.style.fontSize = word.fontSize;
570
  label.style.fontWeight = word.fontWeight;
571
+ label.style.fontFamily = selectedFont;
572
 
573
  // Calculate position if not set
574
  if (word.x === null || word.y === null) {
 
611
  setTimeout(() => {
612
  updateLabelPositions();
613
  resizeCanvas();
614
+ createSnapPoints();
615
  }, 0);
616
  }
617
 
 
642
  isDragging = true;
643
  draggedLabel = label;
644
  draggedIndex = index;
645
+ label.classList.add('dragging');
646
 
647
  const rect = label.getBoundingClientRect();
648
  dragStartX = e.clientX - rect.left;
 
669
  let newX = e.clientX - containerRect.left - dragStartX;
670
  let newY = e.clientY - containerRect.top - dragStartY;
671
 
672
+ // Snap to grid if enabled
673
+ if (snapEnabled) {
674
+ const snapPoints = document.querySelectorAll('.snap-point');
675
+ let minDist = Infinity;
676
+ let bestSnap = {x: newX, y: newY};
677
+
678
+ snapPoints.forEach(point => {
679
+ const pointX = parseFloat(point.style.left);
680
+ const pointY = parseFloat(point.style.top);
681
+ const dist = Math.sqrt(Math.pow(newX - pointX, 2) + Math.pow(newY - pointY, 2));
682
+
683
+ if (dist < 30 && dist < minDist) {
684
+ minDist = dist;
685
+ bestSnap = {x: pointX, y: pointY};
686
+ }
687
+ });
688
+
689
+ if (minDist < 30) {
690
+ newX = bestSnap.x;
691
+ newY = bestSnap.y;
692
+ }
693
+ }
694
+
695
  // Constrain to container bounds
696
  newX = Math.max(0, Math.min(newX, container.offsetWidth - draggedLabel.offsetWidth));
697
  newY = Math.max(0, Math.min(newY, container.offsetHeight - draggedLabel.offsetHeight));
 
718
  }
719
 
720
  function stopDrag() {
721
+ if (draggedLabel) {
722
+ draggedLabel.classList.remove('dragging');
723
+ }
724
+
725
  isDragging = false;
726
  draggedLabel = null;
727
  draggedIndex = null;
 
825
  color: "#1e293b",
826
  bgColor: "#ffffff",
827
  fontSize: "0.85rem",
828
+ fontWeight: "500",
829
+ theme: 0,
830
+ icon: ""
831
  });
832
  input.value = '';
833
  renderKeywords();
 
912
  document.getElementById('bgColor').value = keyword.bgColor;
913
  document.getElementById('fontSize').value = keyword.fontSize;
914
  document.getElementById('fontWeight').value = keyword.fontWeight;
915
+ document.getElementById('keywordTheme').value = keyword.theme;
916
 
917
  editor.style.display = 'block';
918
  contextMenu.style.display = 'none';
 
937
  keyword.bgColor = document.getElementById('bgColor').value;
938
  keyword.fontSize = document.getElementById('fontSize').value;
939
  keyword.fontWeight = document.getElementById('fontWeight').value;
940
+ keyword.theme = document.getElementById('keywordTheme').value;
941
 
942
  // Update line styles
943
  lineWidth = document.getElementById('lineWidth').value;
 
947
  document.getElementById('styleEditor').style.display = 'none';
948
  }
949
 
950
+ function toggleGrid() {
951
+ gridEnabled = !gridEnabled;
952
+ document.getElementById('gridLines').style.display = gridEnabled ? 'block' : 'none';
953
+ document.getElementById('gridToggle').classList.toggle('bg-gray-500', !gridEnabled);
954
+ document.getElementById('gridToggle').classList.toggle('bg-yellow-500', gridEnabled);
955
+ }
956
+
957
+ function toggleSnap() {
958
+ snapEnabled = !snapEnabled;
959
+ document.getElementById('snapToggle').classList.toggle('bg-gray-500', !snapEnabled);
960
+ document.getElementById('snapToggle').classList.toggle('bg-pink-500', snapEnabled);
961
+
962
+ if (snapEnabled) {
963
+ createSnapPoints();
964
+ } else {
965
+ document.querySelectorAll('.snap-point').forEach(point => point.remove());
966
+ }
967
+ }
968
+
969
+ function createSnapPoints() {
970
+ // Remove existing snap points
971
+ document.querySelectorAll('.snap-point').forEach(point => point.remove());
972
+
973
+ const container = document.getElementById('target-container');
974
+ const centerX = container.offsetWidth / 2;
975
+ const centerY = container.offsetHeight / 2;
976
+ const radius = Math.min(centerX, centerY) * 0.8;
977
+
978
+ // Create snap points on circle
979
+ for (let i = 0; i < 12; i++) {
980
+ const angle = (i * Math.PI * 2) / 12;
981
+ const x = centerX + radius * Math.cos(angle);
982
+ const y = centerY + radius * Math.sin(angle);
983
+
984
+ const snapPoint = document.createElement('div');
985
+ snapPoint.className = 'snap-point';
986
+ snapPoint.style.left = `${x}px`;
987
+ snapPoint.style.top = `${y}px`;
988
+ container.appendChild(snapPoint);
989
+ }
990
+
991
+ // Create snap points in center
992
+ for (let i = 0; i < 5; i++) {
993
+ const snapPoint = document.createElement('div');
994
+ snapPoint.className = 'snap-point';
995
+ snapPoint.style.left = `${centerX + (i-2) * 20}px`;
996
+ snapPoint.style.top = `${centerY}px`;
997
+ container.appendChild(snapPoint);
998
+
999
+ if (i !== 2) {
1000
+ const snapPointVert = document.createElement('div');
1001
+ snapPointVert.className = 'snap-point';
1002
+ snapPointVert.style.left = `${centerX}px`;
1003
+ snapPointVert.style.top = `${centerY + (i-2) * 20}px`;
1004
+ container.appendChild(snapPointVert);
1005
+ }
1006
+ }
1007
+ }
1008
+
1009
+ function changeLayer() {
1010
+ currentLayer = parseInt(document.getElementById('layerSelect').value);
1011
+ keywords = JSON.parse(JSON.stringify(layers[currentLayer]));
1012
+ renderKeywords();
1013
+ }
1014
+
1015
+ function groupByTheme() {
1016
+ renderKeywords();
1017
+ }
1018
+
1019
+ function addIconToKeyword() {
1020
+ if (selectedKeywordIndex === null) return;
1021
+
1022
+ iconModal.style.display = 'flex';
1023
+ contextMenu.style.display = 'none';
1024
+ }
1025
+
1026
+ function closeIconModal() {
1027
+ iconModal.style.display = 'none';
1028
+ }
1029
+
1030
+ function selectIcon(icon) {
1031
+ selectedIcon = icon;
1032
+ document.querySelectorAll('.icon-option').forEach(opt => {
1033
+ opt.classList.remove('bg-blue-100');
1034
+ });
1035
+ event.currentTarget.classList.add('bg-blue-100');
1036
+ }
1037
+
1038
+ function applyIcon() {
1039
+ if (selectedKeywordIndex === null || !selectedIcon) return;
1040
+
1041
+ keywords[selectedKeywordIndex].icon = selectedIcon;
1042
+ renderKeywords();
1043
+ closeIconModal();
1044
+ }
1045
+
1046
+ function exportToPNG() {
1047
+ const container = document.getElementById('target-container');
1048
+ const canvas = document.createElement('canvas');
1049
+ canvas.width = container.offsetWidth;
1050
+ canvas.height = container.offsetHeight;
1051
+ const ctx = canvas.getContext('2d');
1052
+
1053
+ // Draw background
1054
+ ctx.fillStyle = '#ffffff';
1055
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1056
+
1057
+ // Draw circles
1058
+ const circles = container.querySelectorAll('.circle');
1059
+ circles.forEach(circle => {
1060
+ const rect = circle.getBoundingClientRect();
1061
+ const containerRect = container.getBoundingClientRect();
1062
+
1063
+ const x = rect.left - containerRect.left + rect.width / 2;
1064
+ const y = rect.top - containerRect.top + rect.height / 2;
1065
+ const radius = rect.width / 2;
1066
+
1067
+ ctx.fillStyle = window.getComputedStyle(circle).backgroundColor;
1068
+ ctx.beginPath();
1069
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
1070
+ ctx.fill();
1071
+ });
1072
+
1073
+ // Draw lines
1074
+ if (keywords.length >= 2) {
1075
+ ctx.beginPath();
1076
+ ctx.strokeStyle = lineColor;
1077
+ ctx.lineWidth = lineWidth;
1078
+
1079
+ if (lineStyle === "dashed") {
1080
+ ctx.setLineDash([5, 3]);
1081
+ } else if (lineStyle === "dotted") {
1082
+ ctx.setLineDash([2, 2]);
1083
+ } else {
1084
+ ctx.setLineDash([]);
1085
+ }
1086
+
1087
+ const labels = container.querySelectorAll('.label');
1088
+ const positions = [];
1089
+
1090
+ labels.forEach(label => {
1091
+ const rect = label.getBoundingClientRect();
1092
+ const containerRect = container.getBoundingClientRect();
1093
+
1094
+ positions.push({
1095
+ x: rect.left - containerRect.left + rect.width / 2,
1096
+ y: rect.top - containerRect.top + rect.height / 2
1097
+ });
1098
+ });
1099
+
1100
+ for (let i = 0; i < positions.length; i++) {
1101
+ const next = positions[(i + 1) % positions.length];
1102
+ ctx.moveTo(positions[i].x, positions[i].y);
1103
+ ctx.lineTo(next.x, next.y);
1104
+ }
1105
+
1106
+ ctx.stroke();
1107
+ }
1108
+
1109
+ // Draw labels
1110
+ const labels = container.querySelectorAll('.label');
1111
+ labels.forEach(label => {
1112
+ const rect = label.getBoundingClientRect();
1113
+ const containerRect = container.getBoundingClientRect();
1114
+
1115
+ const x = rect.left - containerRect.left;
1116
+ const y = rect.top - containerRect.top;
1117
+ const width = rect.width;
1118
+ const height = rect.height;
1119
+
1120
+ // Draw background
1121
+ ctx.fillStyle = window.getComputedStyle(label).backgroundColor;
1122
+ ctx.beginPath();
1123
+ ctx.roundRect(x, y, width, height, 16);
1124
+ ctx.fill();
1125
+
1126
+ // Draw text
1127
+ ctx.fillStyle = window.getComputedStyle(label).color;
1128
+ ctx.font = `${window.getComputedStyle(label).fontWeight} ${window.getComputedStyle(label).fontSize} ${window.getComputedStyle(label).fontFamily}`;
1129
+ ctx.textAlign = 'center';
1130
+ ctx.textBaseline = 'middle';
1131
+ ctx.fillText(label.textContent, x + width/2, y + height/2);
1132
+ });
1133
+
1134
+ // Draw notes if any
1135
+ const notes = document.getElementById('userNotes').value;
1136
+ if (notes.trim()) {
1137
+ ctx.fillStyle = '#1e293b';
1138
+ ctx.font = '500 1rem sans-serif';
1139
+ ctx.textAlign = 'center';
1140
+ ctx.textBaseline = 'top';
1141
+
1142
+ const lines = notes.split('\n');
1143
+ const startY = 20;
1144
+
1145
+ lines.forEach((line, i) => {
1146
+ ctx.fillText(line, canvas.width/2, startY + i*20);
1147
+ });
1148
+ }
1149
+
1150
+ // Create download link
1151
+ const link = document.createElement('a');
1152
+ link.download = 'etoile-de-mots.png';
1153
+ link.href = canvas.toDataURL('image/png');
1154
+ link.click();
1155
+ }
1156
+
1157
+ function exportToJSON() {
1158
+ const data = {
1159
+ keywords: keywords,
1160
+ notes: document.getElementById('userNotes').value,
1161
+ lineColor: lineColor,
1162
+ lineWidth: lineWidth,
1163
+ lineStyle: lineStyle
1164
+ };
1165
+
1166
+ const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'});
1167
+ const url = URL.createObjectURL(blob);
1168
+
1169
+ const link = document.createElement('a');
1170
+ link.download = 'etoile-de-mots.json';
1171
+ link.href = url;
1172
+ link.click();
1173
+
1174
+ setTimeout(() => URL.revokeObjectURL(url), 100);
1175
+ }
1176
+
1177
+ function importFromJSON() {
1178
+ const input = document.createElement('input');
1179
+ input.type = 'file';
1180
+ input.accept = '.json';
1181
+
1182
+ input.onchange = e => {
1183
+ const file = e.target.files[0];
1184
+ const reader = new FileReader();
1185
+
1186
+ reader.onload = event => {
1187
+ try {
1188
+ const data = JSON.parse(event.target.result);
1189
+
1190
+ if (data.keywords) {
1191
+ keywords = data.keywords;
1192
+ layers[currentLayer] = JSON.parse(JSON.stringify(keywords));
1193
+
1194
+ if (data.notes) {
1195
+ document.getElementById('userNotes').value = data.notes;
1196
+ }
1197
+
1198
+ if (data.lineColor) lineColor = data.lineColor;
1199
+ if (data.lineWidth) lineWidth = data.lineWidth;
1200
+ if (data.lineStyle) lineStyle = data.lineStyle;
1201
+
1202
+ renderKeywords();
1203
+ }
1204
+ } catch (error) {
1205
+ alert("Erreur lors du chargement du fichier: " + error.message);
1206
+ }
1207
+ };
1208
+
1209
+ reader.readAsText(file);
1210
+ };
1211
+
1212
+ input.click();
1213
+ }
1214
+
1215
  // Initialize
1216
  document.addEventListener('DOMContentLoaded', () => {
1217
  renderKeywords();
 
1239
  if (editModal.style.display === 'flex' && !editModal.querySelector('.edit-modal-content').contains(e.target)) {
1240
  closeEditModal();
1241
  }
1242
+
1243
+ if (iconModal.style.display === 'flex' && !iconModal.querySelector('.edit-modal-content').contains(e.target)) {
1244
+ closeIconModal();
1245
+ }
1246
+ });
1247
+
1248
+ // Keyboard navigation
1249
+ document.addEventListener('keydown', (e) => {
1250
+ if (selectedKeywordIndex === null) return;
1251
+
1252
+ const step = 5;
1253
+ const keyword = keywords[selectedKeywordIndex];
1254
+
1255
+ switch(e.key) {
1256
+ case 'ArrowUp':
1257
+ keyword.y -= step;
1258
+ e.preventDefault();
1259
+ break;
1260
+ case 'ArrowDown':
1261
+ keyword.y += step;
1262
+ e.preventDefault();
1263
+ break;
1264
+ case 'ArrowLeft':
1265
+ keyword.x -= step;
1266
+ e.preventDefault();
1267
+ break;
1268
+ case 'ArrowRight':
1269
+ keyword.x += step;
1270
+ e.preventDefault();
1271
+ break;
1272
+ case 'Delete':
1273
+ deleteSelectedKeyword();
1274
+ e.preventDefault();
1275
+ break;
1276
+ case 'Escape':
1277
+ deselectKeyword();
1278
+ e.preventDefault();
1279
+ break;
1280
+ }
1281
+
1282
+ if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
1283
+ renderKeywords();
1284
+ }
1285
+ });
1286
+
1287
+ // Font selection
1288
+ document.getElementById('fontSelect').addEventListener('change', (e) => {
1289
+ selectedFont = e.target.value;
1290
+ renderKeywords();
1291
  });
1292
  });
1293
  </script>