PierreH commited on
Commit
5187d48
·
verified ·
1 Parent(s): 83ee0e3

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +91 -250
index.html CHANGED
@@ -1,8 +1,8 @@
1
  <!DOCTYPE html>
2
  <html lang="fr">
3
  <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Toile Dynamique Minimaliste</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
@@ -15,19 +15,13 @@
15
  border-radius: 50%;
16
  overflow: visible;
17
  }
18
-
19
- .circle {
20
- position: absolute;
21
- border-radius: 50%;
22
- z-index: 1;
23
- }
24
-
25
  .circle1 { background-color: #fecaca; width: 8%; height: 8%; top: 46%; left: 46%; }
26
  .circle2 { background-color: #fed7aa; width: 20%; height: 20%; top: 40%; left: 40%; }
27
  .circle3 { background-color: #fef08a; width: 35%; height: 35%; top: 32.5%; left: 32.5%; }
28
  .circle4 { background-color: #bbf7d0; width: 55%; height: 55%; top: 22.5%; left: 22.5%; }
29
  .circle5 { background-color: #bfdbfe; width: 75%; height: 75%; top: 12.5%; left: 12.5%; }
30
-
31
  .label {
32
  position: absolute;
33
  font-weight: bold;
@@ -43,28 +37,17 @@
43
  transition: all 0.2s ease;
44
  box-shadow: 0 1px 2px rgba(0,0,0,0.1);
45
  }
46
-
47
- .label:hover {
48
- transform: scale(1.05);
49
- background-color: rgba(255, 255, 255, 0.95);
50
- }
51
-
52
- .label.selected {
53
- background-color: #3b82f6;
54
- color: white;
55
- transform: scale(1.1);
56
- }
57
-
58
  #canvas {
59
  position: absolute;
60
- top: 0;
61
- left: 0;
62
- width: 100%;
63
- height: 100%;
64
  z-index: 6;
65
  pointer-events: none;
66
  }
67
-
68
  .note-input {
69
  resize: none;
70
  font-size: 0.875rem;
@@ -73,31 +56,21 @@
73
  border: none;
74
  outline: none;
75
  }
76
-
77
- .note-input::placeholder {
78
- color: #9ca3af;
79
- }
80
-
81
  .action-btn {
82
  transition: all 0.2s ease;
83
  }
84
-
85
- .action-btn:hover {
86
- transform: translateY(-1px);
87
- }
88
-
89
- .action-btn:active {
90
- transform: translateY(1px);
91
- }
92
  </style>
93
  </head>
94
  <body class="bg-gray-50 min-h-screen flex flex-col items-center justify-center p-4">
95
  <div class="w-full max-w-md bg-white rounded-xl shadow-md p-6 space-y-6">
96
  <h1 class="text-xl font-bold text-center text-gray-700">☆ Mon Étoile de Mots ☆</h1>
97
-
98
- <textarea id="userNotes" class="note-input w-full p-2 rounded-lg bg-gray-50"
99
- placeholder="Écrivez vos notes ici... nom, date, objectifs etc." rows="2"></textarea>
100
-
101
  <div class="target-container bg-white shadow-inner" id="target-container">
102
  <canvas id="canvas"></canvas>
103
  <div class="circle circle5"></div>
@@ -106,45 +79,24 @@
106
  <div class="circle circle2"></div>
107
  <div class="circle circle1"></div>
108
  </div>
109
-
110
  <div class="grid grid-cols-2 gap-3">
111
- <input type="text" id="newKeyword" placeholder="Nouveau mot"
112
- class="col-span-2 p-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
113
-
114
  <div class="flex space-x-2">
115
- <input type="text" id="editKeyword" placeholder="Modifier"
116
- class="flex-1 p-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
117
- <button onclick="updateKeyword()" class="action-btn bg-blue-500 text-white px-3 py-2 rounded-lg hover:bg-blue-600">
118
- Modifier
119
- </button>
120
  </div>
121
-
122
- <button onclick="deleteKeyword()" class="action-btn bg-red-500 text-white py-2 rounded-lg hover:bg-red-600">
123
- Supprimer
124
- </button>
125
-
126
- <button onclick="addKeyword()" class="action-btn bg-green-500 text-white py-2 rounded-lg hover:bg-green-600">
127
- Ajouter
128
- </button>
129
-
130
- <button onclick="saveAsImage()" class="action-btn bg-purple-500 text-white py-2 rounded-lg hover:bg-purple-600">
131
- Enregistrer (Image)
132
- </button>
133
-
134
- <button onclick="saveAsPDF()" class="action-btn bg-indigo-500 text-white py-2 rounded-lg hover:bg-indigo-600">
135
- Enregistrer (PDF)
136
- </button>
137
-
138
- <button onclick="resetKeywords()" class="action-btn bg-gray-500 text-white py-2 rounded-lg hover:bg-gray-600 col-span-2">
139
- Réinitialiser
140
- </button>
141
  </div>
142
  </div>
143
 
144
  <script>
145
- // Initialize jsPDF
146
  const { jsPDF } = window.jspdf;
147
-
148
  let keywords = [
149
  { text: "Identification", x: null, y: null },
150
  { text: "Gestion", x: null, y: null },
@@ -157,179 +109,160 @@
157
  { text: "Motivation", x: null, y: null },
158
  { text: "Recours", x: null, y: null }
159
  ];
160
-
161
  let selectedKeywordIndex = null;
162
  let isDragging = false;
163
  let dragStartX, dragStartY;
164
  let draggedLabel = null;
165
  let draggedIndex = null;
166
-
167
  const canvas = document.getElementById('canvas');
168
  const ctx = canvas.getContext('2d');
169
-
170
  function renderKeywords() {
171
  const container = document.getElementById('target-container');
172
  const centerX = container.offsetWidth / 2;
173
  const centerY = container.offsetHeight / 2;
174
  const radius = Math.min(centerX, centerY) * 0.35;
175
-
176
- // Remove existing labels
177
  container.querySelectorAll('.label').forEach(e => e.remove());
178
-
179
- // Create new labels
180
  keywords.forEach((word, index) => {
181
  const labelWidth = word.text.length * 8 + 16;
182
  const labelHeight = 24;
183
-
184
- // Calculate position if not set
185
  if (word.x === null || word.y === null) {
186
  const angleStep = (2 * Math.PI) / keywords.length;
187
  const angle = index * angleStep - Math.PI / 2;
188
  word.x = centerX + radius * Math.cos(angle) - labelWidth / 2;
189
  word.y = centerY + radius * Math.sin(angle) - labelHeight / 2;
190
  }
191
-
192
  const label = document.createElement('div');
193
  label.className = 'label draggable';
194
  label.style.left = `${word.x}px`;
195
  label.style.top = `${word.y}px`;
196
  label.textContent = word.text;
197
  label.dataset.index = index;
198
-
199
- label.addEventListener('mousedown', (e) => {
200
- startDrag(e, label, index);
 
 
201
  });
202
-
203
- if (index === selectedKeywordIndex) {
204
- label.classList.add('selected');
205
- }
206
-
207
  container.appendChild(label);
208
  });
209
-
210
  resizeCanvas();
211
  }
212
-
213
  function startDrag(e, label, index) {
214
  isDragging = true;
215
  draggedLabel = label;
216
  draggedIndex = index;
217
  dragStartX = e.clientX - parseFloat(label.style.left);
218
  dragStartY = e.clientY - parseFloat(label.style.top);
219
-
220
- // Select the label being dragged
221
  selectKeyword(index);
222
-
223
  document.addEventListener('mousemove', drag);
224
  document.addEventListener('mouseup', stopDrag);
225
-
226
  e.preventDefault();
227
  }
228
-
229
  function drag(e) {
230
  if (!isDragging) return;
231
-
232
  const container = document.getElementById('target-container');
233
- const containerRect = container.getBoundingClientRect();
234
-
235
- // Calculate new position
236
  let newX = e.clientX - dragStartX;
237
  let newY = e.clientY - dragStartY;
238
-
239
- // Constrain to container bounds
240
  newX = Math.max(0, Math.min(newX, container.offsetWidth - draggedLabel.offsetWidth));
241
  newY = Math.max(0, Math.min(newY, container.offsetHeight - draggedLabel.offsetHeight));
242
-
243
  draggedLabel.style.left = `${newX}px`;
244
  draggedLabel.style.top = `${newY}px`;
245
-
246
- // Update keyword position
247
  keywords[draggedIndex].x = newX;
248
  keywords[draggedIndex].y = newY;
249
-
250
  resizeCanvas();
251
  }
252
-
253
  function stopDrag() {
254
  isDragging = false;
255
  draggedLabel = null;
256
  draggedIndex = null;
257
-
258
  document.removeEventListener('mousemove', drag);
259
  document.removeEventListener('mouseup', stopDrag);
260
  }
261
-
262
  function selectKeyword(index) {
263
  selectedKeywordIndex = index;
264
  document.getElementById('editKeyword').value = keywords[index].text;
265
-
266
- // Update UI
267
- document.querySelectorAll('.label').forEach(label => {
268
- label.classList.remove('selected');
269
- });
270
-
271
  const selectedLabel = document.querySelector(`.label[data-index='${index}']`);
272
- if (selectedLabel) {
273
- selectedLabel.classList.add('selected');
274
- }
275
  }
276
-
277
  function resizeCanvas() {
278
  const container = document.getElementById('target-container');
279
  canvas.width = container.offsetWidth;
280
  canvas.height = container.offsetHeight;
281
  drawLines();
282
  }
283
-
284
  function drawLines() {
285
  ctx.clearRect(0, 0, canvas.width, canvas.height);
286
-
287
  if (keywords.length < 2) return;
288
-
289
  ctx.beginPath();
290
  ctx.strokeStyle = '#3b82f6';
291
  ctx.lineWidth = 1;
292
  ctx.setLineDash([5, 3]);
293
-
294
- const positions = keywords.map(k => {
 
295
  return {
296
- x: k.x + (document.querySelector(`.label[data-index='${keywords.indexOf(k)}']`)?.offsetWidth / 2 || 0),
297
- y: k.y + (document.querySelector(`.label[data-index='${keywords.indexOf(k)}']`)?.offsetHeight / 2 || 0)
298
  };
299
  });
300
-
301
- // Draw connecting lines
302
  for (let i = 0; i < positions.length; i++) {
303
  const next = positions[(i + 1) % positions.length];
304
  ctx.moveTo(positions[i].x, positions[i].y);
305
  ctx.lineTo(next.x, next.y);
306
  }
307
-
308
  ctx.stroke();
309
  }
310
-
311
  function addKeyword() {
312
  const input = document.getElementById('newKeyword');
313
  const text = input.value.trim();
314
-
315
  if (text) {
316
- keywords.push({ text: text, x: null, y: null });
317
  input.value = '';
318
  renderKeywords();
319
  }
320
  }
321
-
322
  function updateKeyword() {
323
  const input = document.getElementById('editKeyword');
324
  const text = input.value.trim();
325
-
326
  if (text && selectedKeywordIndex !== null) {
327
  keywords[selectedKeywordIndex].text = text;
328
  renderKeywords();
329
  selectKeyword(selectedKeywordIndex);
330
  }
331
  }
332
-
333
  function deleteKeyword() {
334
  if (selectedKeywordIndex !== null) {
335
  keywords.splice(selectedKeywordIndex, 1);
@@ -338,120 +271,28 @@
338
  renderKeywords();
339
  }
340
  }
341
-
342
  function resetKeywords() {
343
- keywords.forEach(word => {
344
- word.x = null;
345
- word.y = null;
346
- });
347
  renderKeywords();
348
  }
349
-
350
- async function saveAsImage() {
351
- const buttons = document.querySelectorAll('button, input');
352
- buttons.forEach(btn => btn.style.visibility = 'hidden');
353
-
354
- const note = document.getElementById('userNotes');
355
- const originalStyles = {
356
- textAlign: note.style.textAlign,
357
- fontSize: note.style.fontSize,
358
- fontWeight: note.style.fontWeight,
359
- backgroundColor: note.style.backgroundColor,
360
- border: note.style.border,
361
- boxShadow: note.style.boxShadow
362
- };
363
-
364
- note.style.textAlign = 'center';
365
- note.style.fontSize = '18px';
366
- note.style.fontWeight = 'bold';
367
- note.style.backgroundColor = 'transparent';
368
- note.style.border = 'none';
369
- note.style.boxShadow = 'none';
370
-
371
- try {
372
- const canvas = await html2canvas(document.querySelector('.w-full.max-w-md'), {
373
- scale: 2,
374
- backgroundColor: '#f9fafb',
375
- useCORS: true,
376
- logging: false
377
- });
378
-
379
- const link = document.createElement('a');
380
- link.download = `etoile_mots_${new Date().toISOString().split('T')[0]}.png`;
381
- link.href = canvas.toDataURL('image/png');
382
- document.body.appendChild(link);
383
- link.click();
384
- document.body.removeChild(link);
385
- } catch (err) {
386
- console.error("Erreur lors de l'export:", err);
387
- } finally {
388
- buttons.forEach(btn => btn.style.visibility = 'visible');
389
-
390
- Object.keys(originalStyles).forEach(style => {
391
- note.style[style] = originalStyles[style];
392
- });
393
- }
394
- }
395
-
396
- async function saveAsPDF() {
397
- const buttons = document.querySelectorAll('button, input');
398
- buttons.forEach(btn => btn.style.visibility = 'hidden');
399
-
400
- const note = document.getElementById('userNotes');
401
- const originalStyles = {
402
- textAlign: note.style.textAlign,
403
- fontSize: note.style.fontSize,
404
- fontWeight: note.style.fontWeight,
405
- backgroundColor: note.style.backgroundColor,
406
- border: note.style.border,
407
- boxShadow: note.style.boxShadow
408
- };
409
-
410
- note.style.textAlign = 'center';
411
- note.style.fontSize = '18px';
412
- note.style.fontWeight = 'bold';
413
- note.style.backgroundColor = 'transparent';
414
- note.style.border = 'none';
415
- note.style.boxShadow = 'none';
416
-
417
- try {
418
- const canvas = await html2canvas(document.querySelector('.w-full.max-w-md'), {
419
- scale: 2,
420
- backgroundColor: '#f9fafb',
421
- useCORS: true,
422
- logging: false
423
- });
424
-
425
- const imgData = canvas.toDataURL('image/png');
426
- const pdf = new jsPDF({
427
- orientation: 'portrait',
428
- unit: 'mm'
429
- });
430
-
431
- const imgWidth = 190;
432
- const imgHeight = (canvas.height * imgWidth) / canvas.width;
433
-
434
- pdf.addImage(imgData, 'PNG', 10, 10, imgWidth, imgHeight);
435
- pdf.save(`etoile_mots_${new Date().toISOString().split('T')[0]}.pdf`);
436
- } catch (err) {
437
- console.error("Erreur lors de l'export PDF:", err);
438
- } finally {
439
- buttons.forEach(btn => btn.style.visibility = 'visible');
440
-
441
- Object.keys(originalStyles).forEach(style => {
442
- note.style[style] = originalStyles[style];
443
- });
444
  }
445
- }
446
-
447
- // Initialize
448
- document.addEventListener('DOMContentLoaded', () => {
449
- renderKeywords();
450
- });
451
-
452
- window.addEventListener('resize', () => {
453
- renderKeywords();
454
- });
455
  </script>
456
- <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=PierreH/star" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
457
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="fr">
3
  <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
  <title>Toile Dynamique Minimaliste</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
 
15
  border-radius: 50%;
16
  overflow: visible;
17
  }
18
+ .circle { position: absolute; border-radius: 50%; z-index: 1; }
 
 
 
 
 
 
19
  .circle1 { background-color: #fecaca; width: 8%; height: 8%; top: 46%; left: 46%; }
20
  .circle2 { background-color: #fed7aa; width: 20%; height: 20%; top: 40%; left: 40%; }
21
  .circle3 { background-color: #fef08a; width: 35%; height: 35%; top: 32.5%; left: 32.5%; }
22
  .circle4 { background-color: #bbf7d0; width: 55%; height: 55%; top: 22.5%; left: 22.5%; }
23
  .circle5 { background-color: #bfdbfe; width: 75%; height: 75%; top: 12.5%; left: 12.5%; }
24
+
25
  .label {
26
  position: absolute;
27
  font-weight: bold;
 
37
  transition: all 0.2s ease;
38
  box-shadow: 0 1px 2px rgba(0,0,0,0.1);
39
  }
40
+ .label:hover { transform: scale(1.05); background-color: rgba(255,255,255,0.95); }
41
+ .label.selected { background-color: #3b82f6; color: white; transform: scale(1.1); }
42
+
 
 
 
 
 
 
 
 
 
43
  #canvas {
44
  position: absolute;
45
+ top: 0; left: 0;
46
+ width: 100%; height: 100%;
 
 
47
  z-index: 6;
48
  pointer-events: none;
49
  }
50
+
51
  .note-input {
52
  resize: none;
53
  font-size: 0.875rem;
 
56
  border: none;
57
  outline: none;
58
  }
59
+
60
+ .note-input::placeholder { color: #9ca3af; }
61
+
 
 
62
  .action-btn {
63
  transition: all 0.2s ease;
64
  }
65
+
66
+ .action-btn:hover { transform: translateY(-1px); }
67
+ .action-btn:active { transform: translateY(1px); }
 
 
 
 
 
68
  </style>
69
  </head>
70
  <body class="bg-gray-50 min-h-screen flex flex-col items-center justify-center p-4">
71
  <div class="w-full max-w-md bg-white rounded-xl shadow-md p-6 space-y-6">
72
  <h1 class="text-xl font-bold text-center text-gray-700">☆ Mon Étoile de Mots ☆</h1>
73
+ <textarea id="userNotes" class="note-input w-full p-2 rounded-lg bg-gray-50" placeholder="Écrivez vos notes ici..." rows="2"></textarea>
 
 
 
74
  <div class="target-container bg-white shadow-inner" id="target-container">
75
  <canvas id="canvas"></canvas>
76
  <div class="circle circle5"></div>
 
79
  <div class="circle circle2"></div>
80
  <div class="circle circle1"></div>
81
  </div>
82
+
83
  <div class="grid grid-cols-2 gap-3">
84
+ <input type="text" id="newKeyword" placeholder="Nouveau mot" class="col-span-2 p-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
 
 
85
  <div class="flex space-x-2">
86
+ <input type="text" id="editKeyword" placeholder="Modifier" class="flex-1 p-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
87
+ <button onclick="updateKeyword()" class="action-btn bg-blue-500 text-white px-3 py-2 rounded-lg hover:bg-blue-600">Modifier</button>
 
 
 
88
  </div>
89
+ <button onclick="deleteKeyword()" class="action-btn bg-red-500 text-white py-2 rounded-lg hover:bg-red-600">Supprimer</button>
90
+ <button onclick="addKeyword()" class="action-btn bg-green-500 text-white py-2 rounded-lg hover:bg-green-600">Ajouter</button>
91
+ <button onclick="saveAsImage()" class="action-btn bg-purple-500 text-white py-2 rounded-lg hover:bg-purple-600">Enregistrer (Image)</button>
92
+ <button onclick="saveAsPDF()" class="action-btn bg-indigo-500 text-white py-2 rounded-lg hover:bg-indigo-600">Enregistrer (PDF)</button>
93
+ <button onclick="resetKeywords()" class="action-btn bg-gray-500 text-white py-2 rounded-lg hover:bg-gray-600 col-span-2">Réinitialiser</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  </div>
95
  </div>
96
 
97
  <script>
 
98
  const { jsPDF } = window.jspdf;
99
+
100
  let keywords = [
101
  { text: "Identification", x: null, y: null },
102
  { text: "Gestion", x: null, y: null },
 
109
  { text: "Motivation", x: null, y: null },
110
  { text: "Recours", x: null, y: null }
111
  ];
112
+
113
  let selectedKeywordIndex = null;
114
  let isDragging = false;
115
  let dragStartX, dragStartY;
116
  let draggedLabel = null;
117
  let draggedIndex = null;
118
+
119
  const canvas = document.getElementById('canvas');
120
  const ctx = canvas.getContext('2d');
121
+
122
  function renderKeywords() {
123
  const container = document.getElementById('target-container');
124
  const centerX = container.offsetWidth / 2;
125
  const centerY = container.offsetHeight / 2;
126
  const radius = Math.min(centerX, centerY) * 0.35;
127
+
 
128
  container.querySelectorAll('.label').forEach(e => e.remove());
129
+
 
130
  keywords.forEach((word, index) => {
131
  const labelWidth = word.text.length * 8 + 16;
132
  const labelHeight = 24;
133
+
 
134
  if (word.x === null || word.y === null) {
135
  const angleStep = (2 * Math.PI) / keywords.length;
136
  const angle = index * angleStep - Math.PI / 2;
137
  word.x = centerX + radius * Math.cos(angle) - labelWidth / 2;
138
  word.y = centerY + radius * Math.sin(angle) - labelHeight / 2;
139
  }
140
+
141
  const label = document.createElement('div');
142
  label.className = 'label draggable';
143
  label.style.left = `${word.x}px`;
144
  label.style.top = `${word.y}px`;
145
  label.textContent = word.text;
146
  label.dataset.index = index;
147
+
148
+ label.addEventListener('mousedown', (e) => startDrag(e, label, index));
149
+ label.addEventListener('touchstart', (e) => {
150
+ const touch = e.touches[0];
151
+ startDrag(touch, label, index);
152
  });
153
+
154
+ if (index === selectedKeywordIndex) label.classList.add('selected');
155
+
 
 
156
  container.appendChild(label);
157
  });
158
+
159
  resizeCanvas();
160
  }
161
+
162
  function startDrag(e, label, index) {
163
  isDragging = true;
164
  draggedLabel = label;
165
  draggedIndex = index;
166
  dragStartX = e.clientX - parseFloat(label.style.left);
167
  dragStartY = e.clientY - parseFloat(label.style.top);
168
+
 
169
  selectKeyword(index);
170
+
171
  document.addEventListener('mousemove', drag);
172
  document.addEventListener('mouseup', stopDrag);
173
+
174
  e.preventDefault();
175
  }
176
+
177
  function drag(e) {
178
  if (!isDragging) return;
179
+
180
  const container = document.getElementById('target-container');
 
 
 
181
  let newX = e.clientX - dragStartX;
182
  let newY = e.clientY - dragStartY;
183
+
 
184
  newX = Math.max(0, Math.min(newX, container.offsetWidth - draggedLabel.offsetWidth));
185
  newY = Math.max(0, Math.min(newY, container.offsetHeight - draggedLabel.offsetHeight));
186
+
187
  draggedLabel.style.left = `${newX}px`;
188
  draggedLabel.style.top = `${newY}px`;
189
+
 
190
  keywords[draggedIndex].x = newX;
191
  keywords[draggedIndex].y = newY;
192
+
193
  resizeCanvas();
194
  }
195
+
196
  function stopDrag() {
197
  isDragging = false;
198
  draggedLabel = null;
199
  draggedIndex = null;
200
+
201
  document.removeEventListener('mousemove', drag);
202
  document.removeEventListener('mouseup', stopDrag);
203
  }
204
+
205
  function selectKeyword(index) {
206
  selectedKeywordIndex = index;
207
  document.getElementById('editKeyword').value = keywords[index].text;
208
+ document.querySelectorAll('.label').forEach(label => label.classList.remove('selected'));
 
 
 
 
 
209
  const selectedLabel = document.querySelector(`.label[data-index='${index}']`);
210
+ if (selectedLabel) selectedLabel.classList.add('selected');
 
 
211
  }
212
+
213
  function resizeCanvas() {
214
  const container = document.getElementById('target-container');
215
  canvas.width = container.offsetWidth;
216
  canvas.height = container.offsetHeight;
217
  drawLines();
218
  }
219
+
220
  function drawLines() {
221
  ctx.clearRect(0, 0, canvas.width, canvas.height);
 
222
  if (keywords.length < 2) return;
223
+
224
  ctx.beginPath();
225
  ctx.strokeStyle = '#3b82f6';
226
  ctx.lineWidth = 1;
227
  ctx.setLineDash([5, 3]);
228
+
229
+ const positions = keywords.map((k, i) => {
230
+ const label = document.querySelector(`.label[data-index='${i}']`);
231
  return {
232
+ x: k.x + (label?.offsetWidth || 0) / 2,
233
+ y: k.y + (label?.offsetHeight || 0) / 2
234
  };
235
  });
236
+
 
237
  for (let i = 0; i < positions.length; i++) {
238
  const next = positions[(i + 1) % positions.length];
239
  ctx.moveTo(positions[i].x, positions[i].y);
240
  ctx.lineTo(next.x, next.y);
241
  }
242
+
243
  ctx.stroke();
244
  }
245
+
246
  function addKeyword() {
247
  const input = document.getElementById('newKeyword');
248
  const text = input.value.trim();
 
249
  if (text) {
250
+ keywords.push({ text, x: null, y: null });
251
  input.value = '';
252
  renderKeywords();
253
  }
254
  }
255
+
256
  function updateKeyword() {
257
  const input = document.getElementById('editKeyword');
258
  const text = input.value.trim();
 
259
  if (text && selectedKeywordIndex !== null) {
260
  keywords[selectedKeywordIndex].text = text;
261
  renderKeywords();
262
  selectKeyword(selectedKeywordIndex);
263
  }
264
  }
265
+
266
  function deleteKeyword() {
267
  if (selectedKeywordIndex !== null) {
268
  keywords.splice(selectedKeywordIndex, 1);
 
271
  renderKeywords();
272
  }
273
  }
274
+
275
  function resetKeywords() {
276
+ keywords.forEach(word => { word.x = null; word.y = null; });
 
 
 
277
  renderKeywords();
278
  }
279
+
280
+ async function saveAsImage() { /* ... */ }
281
+ async function saveAsPDF() { /* ... */ }
282
+
283
+ // Support drag tactile
284
+ document.addEventListener('touchmove', function(e) {
285
+ if (isDragging) {
286
+ const touch = e.touches[0];
287
+ drag(touch);
288
+ e.preventDefault();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  }
290
+ }, { passive: false });
291
+
292
+ document.addEventListener('touchend', stopDrag);
293
+
294
+ document.addEventListener('DOMContentLoaded', renderKeywords);
295
+ window.addEventListener('resize', renderKeywords);
 
 
 
 
296
  </script>
297
+ </body>
298
  </html>