star / index.html
PierreH's picture
Add 1 files
0771ac6 verified
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Étoile de Mots Interactive Pro</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.target-container {
position: relative;
width: 100%;
aspect-ratio: 1/1;
border-radius: 50%;
overflow: visible;
touch-action: none;
background: radial-gradient(circle, #f8fafc 0%, #e2e8f0 100%);
}
.circle {
position: absolute;
border-radius: 50%;
z-index: 1;
opacity: 0.7;
transition: all 0.5s ease;
}
.circle1 { background-color: #fecaca; width: 8%; height: 8%; top: 46%; left: 46%; }
.circle2 { background-color: #fed7aa; width: 20%; height: 20%; top: 40%; left: 40%; }
.circle3 { background-color: #fef08a; width: 35%; height: 35%; top: 32.5%; left: 32.5%; }
.circle4 { background-color: #bbf7d0; width: 55%; height: 55%; top: 22.5%; left: 22.5%; }
.circle5 { background-color: #bfdbfe; width: 75%; height: 75%; top: 12.5%; left: 12.5%; }
.label {
position: absolute;
font-weight: 500;
font-size: 0.85rem;
color: #1e293b;
cursor: move;
user-select: none;
white-space: nowrap;
z-index: 7;
padding: 0.35rem 0.7rem;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 1rem;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
border: 1px solid rgba(0,0,0,0.05);
}
.label:hover {
transform: scale(1.05);
box-shadow: 0 4px 6px rgba(0,0,0,0.15);
z-index: 10;
}
.label.selected {
background-color: #3b82f6;
color: white;
transform: scale(1.1);
box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3);
z-index: 10;
}
.label.dragging {
box-shadow: 0 0 15px rgba(0,0,0,0.2);
z-index: 20;
opacity: 0.9;
}
#canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 6;
pointer-events: none;
}
.note-input {
resize: none;
font-size: 0.9rem;
text-align: center;
background-color: transparent;
border: none;
outline: none;
padding: 0.5rem;
border-radius: 0.5rem;
background-color: rgba(255,255,255,0.7);
}
.note-input::placeholder {
color: #94a3b8;
font-style: italic;
}
.action-btn {
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.action-btn:hover {
transform: translateY(-1px);
}
.action-btn:active {
transform: translateY(1px);
}
.context-menu {
position: absolute;
background: white;
border-radius: 0.5rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 100;
min-width: 180px;
overflow: hidden;
display: none;
}
.context-menu-item {
padding: 0.5rem 1rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
}
.context-menu-item:hover {
background-color: #f1f5f9;
}
.style-editor {
background: white;
border-radius: 0.75rem;
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
padding: 1rem;
margin-top: 1rem;
}
.style-option {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.75rem;
}
.color-option {
width: 24px;
height: 24px;
border-radius: 50%;
cursor: pointer;
border: 2px solid transparent;
}
.color-option.selected {
border-color: #1e293b;
}
.color-palette {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.edit-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
display: none;
}
.edit-modal-content {
background: white;
border-radius: 0.75rem;
padding: 1.5rem;
width: 90%;
max-width: 400px;
}
.grid-lines {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2;
pointer-events: none;
background-image:
linear-gradient(to right, rgba(0,0,0,0.05) 1px, transparent 1px),
linear-gradient(to bottom, rgba(0,0,0,0.05) 1px, transparent 1px);
background-size: 20px 20px;
}
.snap-point {
position: absolute;
width: 6px;
height: 6px;
background-color: rgba(59, 130, 246, 0.5);
border-radius: 50%;
z-index: 3;
pointer-events: none;
}
.pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); opacity: 0.7; }
50% { transform: scale(1.02); opacity: 0.9; }
100% { transform: scale(1); opacity: 0.7; }
}
.layer-selector {
position: absolute;
top: 10px;
right: 10px;
z-index: 50;
background: white;
padding: 0.5rem;
border-radius: 0.5rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.theme-group {
position: absolute;
top: 10px;
left: 10px;
z-index: 50;
background: white;
padding: 0.5rem;
border-radius: 0.5rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
@media (max-width: 640px) {
.label {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
.note-input {
font-size: 0.8rem;
}
.action-btn {
padding: 0.5rem;
font-size: 0.8rem;
}
.context-menu {
min-width: 160px;
}
.mobile-hidden {
display: none;
}
}
</style>
</head>
<body class="bg-gradient-to-br from-gray-50 to-gray-100 min-h-screen flex flex-col items-center justify-center p-4">
<div class="w-full max-w-2xl bg-white rounded-xl shadow-lg p-6 space-y-6">
<div class="flex justify-between items-center">
<h1 class="text-2xl font-bold text-center text-gray-800">
<i class="fas fa-star text-yellow-400 mr-2"></i>Étoile de Mots Pro
</h1>
<div class="flex gap-2">
<button onclick="exportToPNG()" class="action-btn bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600">
<i class="fas fa-download"></i>
<span class="mobile-hidden">Exporter</span>
</button>
<button onclick="resetKeywords()" class="action-btn bg-gray-500 text-white px-4 py-2 rounded-lg hover:bg-gray-600">
<i class="fas fa-redo"></i>
<span class="mobile-hidden">Réinit.</span>
</button>
</div>
</div>
<div class="flex gap-2">
<button onclick="exportToJSON()" class="action-btn bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600">
<i class="fas fa-save"></i>
<span class="mobile-hidden">Sauvegarder</span>
</button>
<button onclick="importFromJSON()" class="action-btn bg-purple-500 text-white px-4 py-2 rounded-lg hover:bg-purple-600">
<i class="fas fa-folder-open"></i>
<span class="mobile-hidden">Charger</span>
</button>
<button onclick="toggleGrid()" id="gridToggle" class="action-btn bg-yellow-500 text-white px-4 py-2 rounded-lg hover:bg-yellow-600">
<i class="fas fa-th"></i>
<span class="mobile-hidden">Grille</span>
</button>
<button onclick="toggleSnap()" id="snapToggle" class="action-btn bg-pink-500 text-white px-4 py-2 rounded-lg hover:bg-pink-600">
<i class="fas fa-magnet"></i>
<span class="mobile-hidden">Magnétisme</span>
</button>
</div>
<textarea id="userNotes" class="note-input w-full"
placeholder="Titre, notes ou objectifs... ✍️" rows="2"></textarea>
<div class="target-container shadow-inner" id="target-container">
<div class="grid-lines" id="gridLines" style="display: none;"></div>
<canvas id="canvas"></canvas>
<div class="circle circle5 pulse"></div>
<div class="circle circle4 pulse"></div>
<div class="circle circle3 pulse"></div>
<div class="circle circle2 pulse"></div>
<div class="circle circle1 pulse"></div>
<div class="layer-selector">
<select id="layerSelect" class="border rounded p-1 text-sm" onchange="changeLayer()">
<option value="0">Couche 1</option>
<option value="1">Couche 2</option>
<option value="2">Couche 3</option>
</select>
</div>
<div class="theme-group">
<select id="themeSelect" class="border rounded p-1 text-sm" onchange="groupByTheme()">
<option value="">Tous les thèmes</option>
<option value="1">Thème 1</option>
<option value="2">Thème 2</option>
<option value="3">Thème 3</option>
</select>
</div>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
<input type="text" id="newKeyword" placeholder="Nouveau mot"
class="col-span-2 sm:col-span-1 p-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<button onclick="addKeyword()" class="action-btn bg-green-500 text-white py-2 rounded-lg hover:bg-green-600">
<i class="fas fa-plus"></i>
<span>Ajouter</span>
</button>
<select id="fontSelect" class="border rounded p-2">
<option value="'Segoe UI', Roboto, sans-serif">Sans-serif</option>
<option value="Georgia, 'Times New Roman', serif">Serif</option>
<option value="'Comic Sans MS', cursive">Manuscrite</option>
</select>
</div>
<div class="context-menu" id="contextMenu">
<div class="context-menu-item" onclick="showEditModal()">
<i class="fas fa-edit text-blue-500"></i> Modifier
</div>
<div class="context-menu-item" onclick="deleteSelectedKeyword()">
<i class="fas fa-trash text-red-500"></i> Supprimer
</div>
<div class="context-menu-item" onclick="showStyleEditor()">
<i class="fas fa-paint-brush text-purple-500"></i> Style
</div>
<div class="context-menu-item" onclick="addIconToKeyword()">
<i class="fas fa-icons text-yellow-500"></i> Ajouter icône
</div>
<div class="context-menu-item" onclick="deselectKeyword()">
<i class="fas fa-times-circle text-gray-500"></i> Désélectionner
</div>
</div>
<div class="edit-modal" id="editModal">
<div class="edit-modal-content">
<h3 class="font-bold text-lg mb-4">Modifier le mot</h3>
<input type="text" id="editKeywordInput" class="w-full p-2 border rounded mb-4">
<div class="flex justify-end gap-2">
<button onclick="closeEditModal()" class="px-4 py-2 rounded-lg border">Annuler</button>
<button onclick="updateKeyword()" class="px-4 py-2 rounded-lg bg-blue-500 text-white">Enregistrer</button>
</div>
</div>
</div>
<div class="style-editor" id="styleEditor" style="display: none;">
<h3 class="font-medium text-gray-700 mb-3">Style du mot sélectionné</h3>
<div class="style-option">
<span>Couleur du texte:</span>
<input type="color" id="textColor" value="#1e293b" class="w-8 h-8">
</div>
<div class="style-option">
<span>Couleur de fond:</span>
<input type="color" id="bgColor" value="#ffffff" class="w-8 h-8">
</div>
<div class="style-option">
<span>Taille de police:</span>
<select id="fontSize" class="border rounded p-1">
<option value="0.75rem">Petit</option>
<option value="0.85rem" selected>Moyen</option>
<option value="1rem">Grand</option>
<option value="1.2rem">Très grand</option>
</select>
</div>
<div class="style-option">
<span>Style de police:</span>
<select id="fontWeight" class="border rounded p-1">
<option value="400">Normal</option>
<option value="500" selected>Medium</option>
<option value="600">Gras</option>
<option value="700">Très gras</option>
</select>
</div>
<div class="style-option">
<span>Thème:</span>
<select id="keywordTheme" class="border rounded p-1">
<option value="0">Aucun</option>
<option value="1">Thème 1</option>
<option value="2">Thème 2</option>
<option value="3">Thème 3</option>
</select>
</div>
<div class="mt-4">
<span class="block mb-2">Style des liens:</span>
<div class="color-palette">
<div class="color-option bg-blue-500 selected" data-color="#3b82f6" onclick="selectLineColor(this)"></div>
<div class="color-option bg-red-500" data-color="#ef4444" onclick="selectLineColor(this)"></div>
<div class="color-option bg-green-500" data-color="#10b981" onclick="selectLineColor(this)"></div>
<div class="color-option bg-purple-500" data-color="#8b5cf6" onclick="selectLineColor(this)"></div>
<div class="color-option bg-pink-500" data-color="#ec4899" onclick="selectLineColor(this)"></div>
<div class="color-option bg-gray-900" data-color="#111827" onclick="selectLineColor(this)"></div>
</div>
<div class="mt-3">
<span class="block mb-1">Épaisseur:</span>
<input type="range" id="lineWidth" min="1" max="5" value="1" class="w-full">
</div>
<div class="mt-3">
<span class="block mb-1">Style:</span>
<select id="lineStyle" class="border rounded p-1 w-full">
<option value="solid">Continue</option>
<option value="dashed">Pointillés</option>
<option value="dotted">Pointillés fins</option>
</select>
</div>
</div>
<button onclick="applyStyles()" class="mt-4 w-full bg-blue-500 text-white py-2 rounded-lg hover:bg-blue-600">
Appliquer
</button>
</div>
<div class="edit-modal" id="iconModal" style="display: none;">
<div class="edit-modal-content">
<h3 class="font-bold text-lg mb-4">Ajouter une icône</h3>
<div class="grid grid-cols-4 gap-2 mb-4">
<div class="icon-option p-2 text-center rounded hover:bg-gray-100 cursor-pointer" onclick="selectIcon('fa-heart')">
<i class="fas fa-heart text-red-500 text-xl"></i>
</div>
<div class="icon-option p-2 text-center rounded hover:bg-gray-100 cursor-pointer" onclick="selectIcon('fa-lightbulb')">
<i class="fas fa-lightbulb text-yellow-500 text-xl"></i>
</div>
<div class="icon-option p-2 text-center rounded hover:bg-gray-100 cursor-pointer" onclick="selectIcon('fa-flag')">
<i class="fas fa-flag text-blue-500 text-xl"></i>
</div>
<div class="icon-option p-2 text-center rounded hover:bg-gray-100 cursor-pointer" onclick="selectIcon('fa-star')">
<i class="fas fa-star text-purple-500 text-xl"></i>
</div>
<div class="icon-option p-2 text-center rounded hover:bg-gray-100 cursor-pointer" onclick="selectIcon('fa-check')">
<i class="fas fa-check text-green-500 text-xl"></i>
</div>
<div class="icon-option p-2 text-center rounded hover:bg-gray-100 cursor-pointer" onclick="selectIcon('fa-exclamation')">
<i class="fas fa-exclamation text-orange-500 text-xl"></i>
</div>
<div class="icon-option p-2 text-center rounded hover:bg-gray-100 cursor-pointer" onclick="selectIcon('fa-question')">
<i class="fas fa-question text-indigo-500 text-xl"></i>
</div>
<div class="icon-option p-2 text-center rounded hover:bg-gray-100 cursor-pointer" onclick="selectIcon('fa-bolt')">
<i class="fas fa-bolt text-yellow-500 text-xl"></i>
</div>
</div>
<div class="flex justify-end gap-2">
<button onclick="closeIconModal()" class="px-4 py-2 rounded-lg border">Annuler</button>
<button onclick="applyIcon()" class="px-4 py-2 rounded-lg bg-blue-500 text-white">Appliquer</button>
</div>
</div>
</div>
</div>
<script>
let keywords = [
{ text: "Identification", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500", theme: 1, icon: "" },
{ text: "Gestion", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500", theme: 1, icon: "" },
{ text: "Adaptation", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500", theme: 2, icon: "" },
{ text: "Autonomie", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500", theme: 2, icon: "" },
{ text: "Connaissance", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500", theme: 3, icon: "" },
{ text: "Prévention", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500", theme: 3, icon: "" },
{ text: "Équilibre", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500", theme: 1, icon: "" },
{ text: "Stratégies", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500", theme: 2, icon: "" },
{ text: "Motivation", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500", theme: 3, icon: "" },
{ text: "Recours", x: null, y: null, color: "#1e293b", bgColor: "#ffffff", fontSize: "0.85rem", fontWeight: "500", theme: 1, icon: "" }
];
let layers = [
JSON.parse(JSON.stringify(keywords)),
JSON.parse(JSON.stringify(keywords)),
JSON.parse(JSON.stringify(keywords))
];
let selectedKeywordIndex = null;
let isDragging = false;
let dragStartX, dragStartY;
let draggedLabel = null;
let draggedIndex = null;
let lineColor = "#3b82f6";
let lineWidth = 1;
let lineStyle = "solid";
let currentLayer = 0;
let gridEnabled = false;
let snapEnabled = false;
let selectedIcon = "";
let selectedFont = "'Segoe UI', Roboto, sans-serif";
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const contextMenu = document.getElementById('contextMenu');
const editModal = document.getElementById('editModal');
const iconModal = document.getElementById('iconModal');
function renderKeywords() {
const container = document.getElementById('target-container');
const centerX = container.offsetWidth / 2;
const centerY = container.offsetHeight / 2;
const radius = Math.min(centerX, centerY) * 0.35;
// Remove existing labels
container.querySelectorAll('.label').forEach(e => e.remove());
// Get current theme filter
const themeFilter = document.getElementById('themeSelect').value;
// Create new labels
keywords.forEach((word, index) => {
// Skip if theme filter is active and doesn't match
if (themeFilter && word.theme != themeFilter) return;
const label = document.createElement('div');
label.className = 'label draggable';
// Add icon if exists
if (word.icon) {
label.innerHTML = `<i class="fas ${word.icon} mr-1"></i>${word.text}`;
} else {
label.textContent = word.text;
}
label.dataset.index = index;
// Apply custom styles
label.style.color = word.color;
label.style.backgroundColor = word.bgColor;
label.style.fontSize = word.fontSize;
label.style.fontWeight = word.fontWeight;
label.style.fontFamily = selectedFont;
// Calculate position if not set
if (word.x === null || word.y === null) {
const angleStep = (2 * Math.PI) / keywords.length;
const angle = index * angleStep - Math.PI / 2;
word.x = centerX + radius * Math.cos(angle) - label.offsetWidth / 2;
word.y = centerY + radius * Math.sin(angle) - label.offsetHeight / 2;
}
label.style.left = `${word.x}px`;
label.style.top = `${word.y}px`;
label.addEventListener('mousedown', (e) => {
startDrag(e, label, index);
});
label.addEventListener('contextmenu', (e) => {
e.preventDefault();
showContextMenu(e, index);
});
label.addEventListener('touchstart', (e) => {
e.preventDefault();
const touch = e.touches[0];
const mouseEvent = new MouseEvent('mousedown', {
clientX: touch.clientX,
clientY: touch.clientY
});
label.dispatchEvent(mouseEvent);
}, { passive: false });
if (index === selectedKeywordIndex) {
label.classList.add('selected');
}
container.appendChild(label);
});
// Force reflow to ensure labels are rendered before calculating positions
setTimeout(() => {
updateLabelPositions();
resizeCanvas();
createSnapPoints();
}, 0);
}
function updateLabelPositions() {
const container = document.getElementById('target-container');
const labels = container.querySelectorAll('.label');
labels.forEach((label, index) => {
if (keywords[index].x === null || keywords[index].y === null) {
const rect = label.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
keywords[index].x = rect.left - containerRect.left;
keywords[index].y = rect.top - containerRect.top;
}
});
}
function startDrag(e, label, index) {
// Prevent context menu on drag
if (e.button === 2) return;
// Toggle selection if clicking on same label
if (selectedKeywordIndex === index) {
return;
}
isDragging = true;
draggedLabel = label;
draggedIndex = index;
label.classList.add('dragging');
const rect = label.getBoundingClientRect();
dragStartX = e.clientX - rect.left;
dragStartY = e.clientY - rect.top;
// Select the label being dragged
selectKeyword(index);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', stopDrag);
document.addEventListener('touchmove', touchDrag, { passive: false });
document.addEventListener('touchend', stopDrag);
e.preventDefault();
}
function drag(e) {
if (!isDragging || !draggedLabel) return;
const container = document.getElementById('target-container');
const containerRect = container.getBoundingClientRect();
// Calculate new position relative to container
let newX = e.clientX - containerRect.left - dragStartX;
let newY = e.clientY - containerRect.top - dragStartY;
// Snap to grid if enabled
if (snapEnabled) {
const snapPoints = document.querySelectorAll('.snap-point');
let minDist = Infinity;
let bestSnap = {x: newX, y: newY};
snapPoints.forEach(point => {
const pointX = parseFloat(point.style.left);
const pointY = parseFloat(point.style.top);
const dist = Math.sqrt(Math.pow(newX - pointX, 2) + Math.pow(newY - pointY, 2));
if (dist < 30 && dist < minDist) {
minDist = dist;
bestSnap = {x: pointX, y: pointY};
}
});
if (minDist < 30) {
newX = bestSnap.x;
newY = bestSnap.y;
}
}
// Constrain to container bounds
newX = Math.max(0, Math.min(newX, container.offsetWidth - draggedLabel.offsetWidth));
newY = Math.max(0, Math.min(newY, container.offsetHeight - draggedLabel.offsetHeight));
draggedLabel.style.left = `${newX}px`;
draggedLabel.style.top = `${newY}px`;
// Update keyword position
keywords[draggedIndex].x = newX;
keywords[draggedIndex].y = newY;
drawLines();
}
function touchDrag(e) {
if (!isDragging || !draggedLabel) return;
const touch = e.touches[0];
const mouseEvent = new MouseEvent('mousemove', {
clientX: touch.clientX,
clientY: touch.clientY
});
drag(mouseEvent);
}
function stopDrag() {
if (draggedLabel) {
draggedLabel.classList.remove('dragging');
}
isDragging = false;
draggedLabel = null;
draggedIndex = null;
document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', stopDrag);
document.removeEventListener('touchmove', touchDrag);
document.removeEventListener('touchend', stopDrag);
}
function selectKeyword(index) {
selectedKeywordIndex = index;
// Update UI
document.querySelectorAll('.label').forEach(label => {
label.classList.remove('selected');
});
const selectedLabel = document.querySelector(`.label[data-index='${index}']`);
if (selectedLabel) {
selectedLabel.classList.add('selected');
}
// Hide style editor when selecting new keyword
document.getElementById('styleEditor').style.display = 'none';
}
function deselectKeyword() {
selectedKeywordIndex = null;
// Update UI
document.querySelectorAll('.label').forEach(label => {
label.classList.remove('selected');
});
// Hide context menu and style editor
contextMenu.style.display = 'none';
document.getElementById('styleEditor').style.display = 'none';
}
function resizeCanvas() {
const container = document.getElementById('target-container');
canvas.width = container.offsetWidth;
canvas.height = container.offsetHeight;
drawLines();
}
function drawLines() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (keywords.length < 2) return;
ctx.beginPath();
ctx.strokeStyle = lineColor;
ctx.lineWidth = lineWidth;
// Set line style
if (lineStyle === "dashed") {
ctx.setLineDash([5, 3]);
} else if (lineStyle === "dotted") {
ctx.setLineDash([2, 2]);
} else {
ctx.setLineDash([]);
}
const labels = document.querySelectorAll('.label');
const positions = [];
labels.forEach(label => {
const index = parseInt(label.dataset.index);
if (!isNaN(index) && index >= 0 && index < keywords.length) {
const rect = label.getBoundingClientRect();
const containerRect = canvas.getBoundingClientRect();
positions.push({
x: rect.left - containerRect.left + rect.width / 2,
y: rect.top - containerRect.top + rect.height / 2
});
}
});
// Draw connecting lines
for (let i = 0; i < positions.length; i++) {
const next = positions[(i + 1) % positions.length];
ctx.moveTo(positions[i].x, positions[i].y);
ctx.lineTo(next.x, next.y);
}
ctx.stroke();
}
function addKeyword() {
const input = document.getElementById('newKeyword');
const text = input.value.trim();
if (text) {
keywords.push({
text: text,
x: null,
y: null,
color: "#1e293b",
bgColor: "#ffffff",
fontSize: "0.85rem",
fontWeight: "500",
theme: 0,
icon: ""
});
input.value = '';
renderKeywords();
}
}
function deleteSelectedKeyword() {
if (selectedKeywordIndex === null) return;
if (confirm("Supprimer ce mot?")) {
keywords.splice(selectedKeywordIndex, 1);
deselectKeyword();
renderKeywords();
}
contextMenu.style.display = 'none';
}
function resetKeywords() {
if (confirm("Réinitialiser tous les mots à leur position par défaut?")) {
keywords.forEach(word => {
word.x = null;
word.y = null;
});
deselectKeyword();
renderKeywords();
}
}
function showContextMenu(e, index) {
e.preventDefault();
selectKeyword(index);
contextMenu.style.display = 'block';
contextMenu.style.left = `${e.clientX}px`;
contextMenu.style.top = `${e.clientY}px`;
// Close menu when clicking elsewhere
setTimeout(() => {
document.addEventListener('click', closeContextMenu);
}, 10);
}
function closeContextMenu() {
contextMenu.style.display = 'none';
document.removeEventListener('click', closeContextMenu);
}
function showEditModal() {
if (selectedKeywordIndex === null) return;
const keyword = keywords[selectedKeywordIndex];
document.getElementById('editKeywordInput').value = keyword.text;
editModal.style.display = 'flex';
contextMenu.style.display = 'none';
}
function closeEditModal() {
editModal.style.display = 'none';
}
function updateKeyword() {
if (selectedKeywordIndex === null) return;
const newText = document.getElementById('editKeywordInput').value.trim();
if (newText) {
keywords[selectedKeywordIndex].text = newText;
renderKeywords();
selectKeyword(selectedKeywordIndex);
}
closeEditModal();
}
function showStyleEditor() {
if (selectedKeywordIndex === null) return;
const editor = document.getElementById('styleEditor');
const keyword = keywords[selectedKeywordIndex];
// Set current values
document.getElementById('textColor').value = keyword.color;
document.getElementById('bgColor').value = keyword.bgColor;
document.getElementById('fontSize').value = keyword.fontSize;
document.getElementById('fontWeight').value = keyword.fontWeight;
document.getElementById('keywordTheme').value = keyword.theme;
editor.style.display = 'block';
contextMenu.style.display = 'none';
}
function selectLineColor(element) {
document.querySelectorAll('.color-option').forEach(opt => {
opt.classList.remove('selected');
});
element.classList.add('selected');
lineColor = element.dataset.color;
drawLines();
}
function applyStyles() {
if (selectedKeywordIndex === null) return;
const keyword = keywords[selectedKeywordIndex];
// Update keyword styles
keyword.color = document.getElementById('textColor').value;
keyword.bgColor = document.getElementById('bgColor').value;
keyword.fontSize = document.getElementById('fontSize').value;
keyword.fontWeight = document.getElementById('fontWeight').value;
keyword.theme = document.getElementById('keywordTheme').value;
// Update line styles
lineWidth = document.getElementById('lineWidth').value;
lineStyle = document.getElementById('lineStyle').value;
renderKeywords();
document.getElementById('styleEditor').style.display = 'none';
}
function toggleGrid() {
gridEnabled = !gridEnabled;
document.getElementById('gridLines').style.display = gridEnabled ? 'block' : 'none';
document.getElementById('gridToggle').classList.toggle('bg-gray-500', !gridEnabled);
document.getElementById('gridToggle').classList.toggle('bg-yellow-500', gridEnabled);
}
function toggleSnap() {
snapEnabled = !snapEnabled;
document.getElementById('snapToggle').classList.toggle('bg-gray-500', !snapEnabled);
document.getElementById('snapToggle').classList.toggle('bg-pink-500', snapEnabled);
if (snapEnabled) {
createSnapPoints();
} else {
document.querySelectorAll('.snap-point').forEach(point => point.remove());
}
}
function createSnapPoints() {
// Remove existing snap points
document.querySelectorAll('.snap-point').forEach(point => point.remove());
const container = document.getElementById('target-container');
const centerX = container.offsetWidth / 2;
const centerY = container.offsetHeight / 2;
const radius = Math.min(centerX, centerY) * 0.8;
// Create snap points on circle
for (let i = 0; i < 12; i++) {
const angle = (i * Math.PI * 2) / 12;
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
const snapPoint = document.createElement('div');
snapPoint.className = 'snap-point';
snapPoint.style.left = `${x}px`;
snapPoint.style.top = `${y}px`;
container.appendChild(snapPoint);
}
// Create snap points in center
for (let i = 0; i < 5; i++) {
const snapPoint = document.createElement('div');
snapPoint.className = 'snap-point';
snapPoint.style.left = `${centerX + (i-2) * 20}px`;
snapPoint.style.top = `${centerY}px`;
container.appendChild(snapPoint);
if (i !== 2) {
const snapPointVert = document.createElement('div');
snapPointVert.className = 'snap-point';
snapPointVert.style.left = `${centerX}px`;
snapPointVert.style.top = `${centerY + (i-2) * 20}px`;
container.appendChild(snapPointVert);
}
}
}
function changeLayer() {
currentLayer = parseInt(document.getElementById('layerSelect').value);
keywords = JSON.parse(JSON.stringify(layers[currentLayer]));
renderKeywords();
}
function groupByTheme() {
renderKeywords();
}
function addIconToKeyword() {
if (selectedKeywordIndex === null) return;
iconModal.style.display = 'flex';
contextMenu.style.display = 'none';
}
function closeIconModal() {
iconModal.style.display = 'none';
}
function selectIcon(icon) {
selectedIcon = icon;
document.querySelectorAll('.icon-option').forEach(opt => {
opt.classList.remove('bg-blue-100');
});
event.currentTarget.classList.add('bg-blue-100');
}
function applyIcon() {
if (selectedKeywordIndex === null || !selectedIcon) return;
keywords[selectedKeywordIndex].icon = selectedIcon;
renderKeywords();
closeIconModal();
}
function exportToPNG() {
const container = document.getElementById('target-container');
const canvas = document.createElement('canvas');
canvas.width = container.offsetWidth;
canvas.height = container.offsetHeight;
const ctx = canvas.getContext('2d');
// Draw background
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw circles
const circles = container.querySelectorAll('.circle');
circles.forEach(circle => {
const rect = circle.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const x = rect.left - containerRect.left + rect.width / 2;
const y = rect.top - containerRect.top + rect.height / 2;
const radius = rect.width / 2;
ctx.fillStyle = window.getComputedStyle(circle).backgroundColor;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
});
// Draw lines
if (keywords.length >= 2) {
ctx.beginPath();
ctx.strokeStyle = lineColor;
ctx.lineWidth = lineWidth;
if (lineStyle === "dashed") {
ctx.setLineDash([5, 3]);
} else if (lineStyle === "dotted") {
ctx.setLineDash([2, 2]);
} else {
ctx.setLineDash([]);
}
const labels = container.querySelectorAll('.label');
const positions = [];
labels.forEach(label => {
const rect = label.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
positions.push({
x: rect.left - containerRect.left + rect.width / 2,
y: rect.top - containerRect.top + rect.height / 2
});
});
for (let i = 0; i < positions.length; i++) {
const next = positions[(i + 1) % positions.length];
ctx.moveTo(positions[i].x, positions[i].y);
ctx.lineTo(next.x, next.y);
}
ctx.stroke();
}
// Draw labels
const labels = container.querySelectorAll('.label');
labels.forEach(label => {
const rect = label.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const x = rect.left - containerRect.left;
const y = rect.top - containerRect.top;
const width = rect.width;
const height = rect.height;
// Draw background
ctx.fillStyle = window.getComputedStyle(label).backgroundColor;
ctx.beginPath();
ctx.roundRect(x, y, width, height, 16);
ctx.fill();
// Draw text
ctx.fillStyle = window.getComputedStyle(label).color;
ctx.font = `${window.getComputedStyle(label).fontWeight} ${window.getComputedStyle(label).fontSize} ${window.getComputedStyle(label).fontFamily}`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(label.textContent, x + width/2, y + height/2);
});
// Draw notes if any
const notes = document.getElementById('userNotes').value;
if (notes.trim()) {
ctx.fillStyle = '#1e293b';
ctx.font = '500 1rem sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
const lines = notes.split('\n');
const startY = 20;
lines.forEach((line, i) => {
ctx.fillText(line, canvas.width/2, startY + i*20);
});
}
// Create download link
const link = document.createElement('a');
link.download = 'etoile-de-mots.png';
link.href = canvas.toDataURL('image/png');
link.click();
}
function exportToJSON() {
const data = {
keywords: keywords,
notes: document.getElementById('userNotes').value,
lineColor: lineColor,
lineWidth: lineWidth,
lineStyle: lineStyle
};
const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.download = 'etoile-de-mots.json';
link.href = url;
link.click();
setTimeout(() => URL.revokeObjectURL(url), 100);
}
function importFromJSON() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = e => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = event => {
try {
const data = JSON.parse(event.target.result);
if (data.keywords) {
keywords = data.keywords;
layers[currentLayer] = JSON.parse(JSON.stringify(keywords));
if (data.notes) {
document.getElementById('userNotes').value = data.notes;
}
if (data.lineColor) lineColor = data.lineColor;
if (data.lineWidth) lineWidth = data.lineWidth;
if (data.lineStyle) lineStyle = data.lineStyle;
renderKeywords();
}
} catch (error) {
alert("Erreur lors du chargement du fichier: " + error.message);
}
};
reader.readAsText(file);
};
input.click();
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
renderKeywords();
// Handle window resize with debounce
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
renderKeywords();
}, 200);
});
// Close context menu when clicking outside
document.addEventListener('click', (e) => {
if (!contextMenu.contains(e.target) && contextMenu.style.display === 'block') {
contextMenu.style.display = 'none';
}
if (!document.getElementById('styleEditor').contains(e.target) &&
document.getElementById('styleEditor').style.display === 'block') {
document.getElementById('styleEditor').style.display = 'none';
}
if (editModal.style.display === 'flex' && !editModal.querySelector('.edit-modal-content').contains(e.target)) {
closeEditModal();
}
if (iconModal.style.display === 'flex' && !iconModal.querySelector('.edit-modal-content').contains(e.target)) {
closeIconModal();
}
});
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (selectedKeywordIndex === null) return;
const step = 5;
const keyword = keywords[selectedKeywordIndex];
switch(e.key) {
case 'ArrowUp':
keyword.y -= step;
e.preventDefault();
break;
case 'ArrowDown':
keyword.y += step;
e.preventDefault();
break;
case 'ArrowLeft':
keyword.x -= step;
e.preventDefault();
break;
case 'ArrowRight':
keyword.x += step;
e.preventDefault();
break;
case 'Delete':
deleteSelectedKeyword();
e.preventDefault();
break;
case 'Escape':
deselectKeyword();
e.preventDefault();
break;
}
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
renderKeywords();
}
});
// Font selection
document.getElementById('fontSelect').addEventListener('change', (e) => {
selectedFont = e.target.value;
renderKeywords();
});
});
</script>
<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>
</html>