import { hexToHsl, hslToHex, rgbToHsl, rgbToHex } from './converter.js'; const MAX_FARBEN = 4; const THUMB_SIZE = 120; const DISTINCT_THRESHOLD = 45; // Mindest-RGB-Abstand damit zwei Farben als verschieden gelten // --- K-Means Farbextraktion --- function colorDist(a, b) { return Math.sqrt((a[0]-b[0])**2 + (a[1]-b[1])**2 + (a[2]-b[2])**2); } function samplePixels(dataUrl, maxSamples, callback) { const img = new Image(); img.onload = () => { const c = document.createElement('canvas'); c.width = img.width; c.height = img.height; const ctx = c.getContext('2d'); ctx.drawImage(img, 0, 0); const data = ctx.getImageData(0, 0, c.width, c.height).data; const total = c.width * c.height; const step = Math.max(1, Math.floor(total / maxSamples)); const pixels = []; for (let i = 0; i < total; i += step) { const idx = i * 4; if (data[idx + 3] < 128) continue; pixels.push([data[idx], data[idx+1], data[idx+2]]); } callback(pixels); }; img.src = dataUrl; } function kMeans(pixels, k, iterations = 15) { if (pixels.length <= k) return pixels.map(p => [...p]); // K-Means++ Initialisierung: erste zufällig, dann maximaler Mindestabstand const centroids = [pixels[Math.floor(Math.random() * pixels.length)].slice()]; for (let c = 1; c < k; c++) { let maxD = -1, best = centroids[0]; for (const px of pixels) { const d = Math.min(...centroids.map(ce => colorDist(px, ce))); if (d > maxD) { maxD = d; best = px; } } centroids.push(best.slice()); } for (let iter = 0; iter < iterations; iter++) { const clusters = Array.from({length: k}, () => []); for (const px of pixels) { let minD = Infinity, minI = 0; for (let i = 0; i < k; i++) { const d = colorDist(px, centroids[i]); if (d < minD) { minD = d; minI = i; } } clusters[minI].push(px); } for (let i = 0; i < k; i++) { if (clusters[i].length === 0) continue; centroids[i] = [0, 1, 2].map(ch => Math.round(clusters[i].reduce((s, p) => s + p[ch], 0) / clusters[i].length) ); } } return centroids; } function extractDominantColors(dataUrl, callback) { samplePixels(dataUrl, 600, (pixels) => { if (pixels.length === 0) { callback([]); return; } const raw = kMeans(pixels, Math.min(3, pixels.length), 15); // Nur hinreichend verschiedene Farben behalten const distinct = [raw[0]]; for (let i = 1; i < raw.length; i++) { if (distinct.every(d => colorDist(raw[i], d) >= DISTINCT_THRESHOLD)) { distinct.push(raw[i]); } } callback(distinct.map(rgb => ({ hex: rgbToHex({ r: rgb[0], g: rgb[1], b: rgb[2] }), hsl: rgbToHsl({ r: rgb[0], g: rgb[1], b: rgb[2] }) }))); }); } // Aktuell gesammelter Zustand des Modals let farben = []; // Array von HSL-Objekten let aktuelleHex = ''; // Hex-Wert des aktuellen Eingabefelds let vorschaubild = null; // Base64-String des Thumbnails oder null let editOriginalName = null; // Name des zu bearbeitenden Schemas (null = neu) let savedOnSave = null; // onSave-Callback aus initSchemaModal function compressToThumbnail(file) { return new Promise((resolve) => { const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { const scale = Math.min(THUMB_SIZE / img.width, THUMB_SIZE / img.height, 1); const w = Math.round(img.width * scale); const h = Math.round(img.height * scale); const c = document.createElement('canvas'); c.width = w; c.height = h; c.getContext('2d').drawImage(img, 0, 0, w, h); resolve(c.toDataURL('image/jpeg', 0.8)); }; img.src = e.target.result; }; reader.readAsDataURL(file); }); } function resetModal() { farben = []; aktuelleHex = ''; vorschaubild = null; document.getElementById('schema-name-input').value = ''; document.getElementById('schema-hex-input').value = ''; document.getElementById('schema-hex-preview').style.background = '#eee'; document.getElementById('schema-farben-preview').textContent = ''; document.getElementById('schema-canvas').style.display = 'none'; document.getElementById('schema-canvas').getContext('2d').clearRect(0, 0, 1, 1); document.getElementById('schema-vorschlag').style.display = 'none'; document.getElementById('schema-farbe-eingabe').style.display = 'none'; document.getElementById('schema-abschliessen-btn').disabled = true; editOriginalName = null; document.getElementById('schema-modal-title').textContent = 'Neues Farbschema'; const preview = document.getElementById('schema-bild-preview'); preview.style.backgroundImage = ''; preview.classList.remove('hat-bild'); document.getElementById('schema-bild-input').value = ''; document.getElementById('schema-bild-entfernen-btn').style.display = 'none'; updateFarbeLabel(); } function showVorschlag(farbenVorschlag) { const container = document.getElementById('schema-vorschlag-farben'); container.textContent = ''; farbenVorschlag.forEach(({ hex }) => { const cell = document.createElement('div'); cell.style.cssText = 'display:inline-flex;flex-direction:column;align-items:center;gap:0.2rem'; const swatch = document.createElement('div'); swatch.style.cssText = 'width:44px;height:44px;border-radius:6px;border:1px solid #ddd;background:' + hex; const label = document.createElement('span'); label.style.cssText = 'font-size:0.7rem;font-family:monospace;color:#666'; label.textContent = hex; cell.appendChild(swatch); cell.appendChild(label); container.appendChild(cell); }); document.getElementById('schema-vorschlag').style.display = 'block'; document.getElementById('schema-farbe-eingabe').style.display = 'none'; return farbenVorschlag; } function updateFarbeLabel() { document.getElementById('schema-farbe-label').textContent = 'Farbe ' + (farben.length + 1) + (farben.length === MAX_FARBEN - 1 ? ' (letzte)' : ''); } function renderFarbenPreview() { const container = document.getElementById('schema-farben-preview'); container.textContent = ''; farben.forEach((hsl, i) => { const hex = hslToHex(hsl); const cell = document.createElement('div'); cell.style.cssText = 'display:inline-flex;flex-direction:column;align-items:center;gap:0.2rem'; const swatch = document.createElement('div'); swatch.style.cssText = 'width:44px;height:44px;border-radius:6px;border:1px solid #ddd;background:' + hex; swatch.title = hex; const label = document.createElement('span'); label.style.cssText = 'font-size:0.7rem;font-family:monospace;color:#666'; label.textContent = hex; const removeBtn = document.createElement('button'); removeBtn.className = 'action-btn'; removeBtn.style.fontSize = '0.65rem'; removeBtn.textContent = '✕'; removeBtn.addEventListener('click', () => { farben.splice(i, 1); renderFarbenPreview(); updateFarbeLabel(); document.getElementById('schema-farbe-eingabe').style.display = 'block'; document.getElementById('schema-abschliessen-btn').disabled = farben.length === 0; }); cell.appendChild(swatch); cell.appendChild(label); cell.appendChild(removeBtn); container.appendChild(cell); }); } function loadImageOnCanvas(file) { const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { const canvas = document.getElementById('schema-canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); canvas.style.display = 'block'; // Mittlerer Pixel als Vorschlag const px = ctx.getImageData(Math.floor(img.width / 2), Math.floor(img.height / 2), 1, 1).data; setAktuelleHex(rgbToHex({ r: px[0], g: px[1], b: px[2] })); canvas.onclick = (ev) => { const rect = canvas.getBoundingClientRect(); const x = Math.floor((ev.clientX - rect.left) * canvas.width / rect.width); const y = Math.floor((ev.clientY - rect.top) * canvas.height / rect.height); const d = ctx.getImageData(x, y, 1, 1).data; setAktuelleHex(rgbToHex({ r: d[0], g: d[1], b: d[2] })); }; }; img.src = e.target.result; }; reader.readAsDataURL(file); } function setAktuelleHex(hex) { aktuelleHex = hex; document.getElementById('schema-hex-input').value = hex; document.getElementById('schema-hex-preview').style.background = hex; } export function openForEdit(schema) { resetModal(); editOriginalName = schema.name; document.getElementById('schema-modal-title').textContent = 'Schema bearbeiten'; document.getElementById('schema-name-input').value = schema.name; if (schema.bild) { vorschaubild = schema.bild; const preview = document.getElementById('schema-bild-preview'); preview.style.backgroundImage = 'url(' + schema.bild + ')'; preview.classList.add('hat-bild'); document.getElementById('schema-bild-entfernen-btn').style.display = 'inline-block'; } farben = schema.farben.map(hsl => ({ ...hsl })); renderFarbenPreview(); document.getElementById('schema-abschliessen-btn').disabled = false; if (farben.length < MAX_FARBEN) { updateFarbeLabel(); document.getElementById('schema-farbe-eingabe').style.display = 'block'; } document.getElementById('schema-modal-overlay').style.display = 'flex'; document.getElementById('schema-name-input').focus(); } export function initSchemaModal(onSave) { savedOnSave = onSave; // Button öffnet Modal document.getElementById('schema-erstellen-btn').addEventListener('click', () => { resetModal(); document.getElementById('schema-modal-overlay').style.display = 'flex'; document.getElementById('schema-name-input').focus(); }); // Abbrechen document.getElementById('schema-abbrechen-btn').addEventListener('click', () => { document.getElementById('schema-modal-overlay').style.display = 'none'; }); // Klick außerhalb schließt Modal document.getElementById('schema-modal-overlay').addEventListener('click', (e) => { if (e.target === document.getElementById('schema-modal-overlay')) { document.getElementById('schema-modal-overlay').style.display = 'none'; } }); // Hex-Eingabe → Vorschau aktualisieren document.getElementById('schema-hex-input').addEventListener('input', (e) => { const val = e.target.value.trim(); if (/^#[0-9a-fA-F]{6}$/.test(val)) { aktuelleHex = val; document.getElementById('schema-hex-preview').style.background = val; } }); // Datei-Upload document.getElementById('schema-file-trigger').addEventListener('click', () => { document.getElementById('schema-file-input').click(); }); document.getElementById('schema-file-input').addEventListener('change', (e) => { if (e.target.files[0]) loadImageOnCanvas(e.target.files[0]); }); // Drag & Drop auf Modal-Dropzone const dropzone = document.getElementById('schema-dropzone'); dropzone.addEventListener('dragover', (e) => { e.preventDefault(); dropzone.style.borderColor = '#222'; }); dropzone.addEventListener('dragleave', () => { dropzone.style.borderColor = '#ccc'; }); dropzone.addEventListener('drop', (e) => { e.preventDefault(); dropzone.style.borderColor = '#ccc'; const file = e.dataTransfer.files[0]; if (file && file.type.startsWith('image/')) loadImageOnCanvas(file); }); // Farbe hinzufügen document.getElementById('schema-farbe-hinzufuegen-btn').addEventListener('click', () => { if (!/^#[0-9a-fA-F]{6}$/.test(aktuelleHex)) { document.getElementById('schema-hex-input').style.borderColor = '#e55'; document.getElementById('schema-hex-input').focus(); return; } document.getElementById('schema-hex-input').style.borderColor = ''; const hsl = hexToHsl(aktuelleHex); if (!hsl) return; farben.push(hsl); renderFarbenPreview(); document.getElementById('schema-abschliessen-btn').disabled = false; // Eingabe zurücksetzen aktuelleHex = ''; document.getElementById('schema-hex-input').value = ''; document.getElementById('schema-hex-preview').style.background = '#eee'; document.getElementById('schema-canvas').style.display = 'none'; document.getElementById('schema-file-input').value = ''; if (farben.length >= MAX_FARBEN) { // Alle 4 Farben gesammelt — Eingabe ausblenden document.getElementById('schema-farbe-eingabe').style.display = 'none'; } else { updateFarbeLabel(); document.getElementById('schema-hex-input').focus(); } }); // Abschließen document.getElementById('schema-abschliessen-btn').addEventListener('click', () => { const name = document.getElementById('schema-name-input').value.trim(); if (!name) { document.getElementById('schema-name-input').style.borderColor = '#e55'; document.getElementById('schema-name-input').focus(); return; } if (farben.length === 0) return; savedOnSave(name, farben, vorschaubild, editOriginalName); document.getElementById('schema-modal-overlay').style.display = 'none'; }); // Vorschaubild hochladen document.getElementById('schema-bild-trigger').addEventListener('click', () => { document.getElementById('schema-bild-input').click(); }); let letzterVorschlag = []; document.getElementById('schema-bild-input').addEventListener('change', async (e) => { const file = e.target.files[0]; if (!file) return; vorschaubild = await compressToThumbnail(file); const preview = document.getElementById('schema-bild-preview'); preview.style.backgroundImage = 'url(' + vorschaubild + ')'; preview.classList.add('hat-bild'); document.getElementById('schema-bild-entfernen-btn').style.display = 'inline-block'; // Farben extrahieren und Vorschlag zeigen extractDominantColors(vorschaubild, (vorschlag) => { if (vorschlag.length > 0) { letzterVorschlag = showVorschlag(vorschlag); } else { // Keine Farben erkannt → direkt manuell document.getElementById('schema-farbe-eingabe').style.display = 'block'; } }); }); // Vorschlag annehmen document.getElementById('schema-vorschlag-annehmen-btn').addEventListener('click', () => { farben = letzterVorschlag.map(v => v.hsl); renderFarbenPreview(); document.getElementById('schema-abschliessen-btn').disabled = false; document.getElementById('schema-vorschlag').style.display = 'none'; if (farben.length < MAX_FARBEN) { updateFarbeLabel(); document.getElementById('schema-farbe-eingabe').style.display = 'block'; document.getElementById('schema-hex-input').focus(); } }); // Vorschlag ablehnen → manuell document.getElementById('schema-vorschlag-ablehnen-btn').addEventListener('click', () => { letzterVorschlag = []; document.getElementById('schema-vorschlag').style.display = 'none'; document.getElementById('schema-farbe-eingabe').style.display = 'block'; document.getElementById('schema-hex-input').focus(); }); document.getElementById('schema-bild-entfernen-btn').addEventListener('click', () => { vorschaubild = null; letzterVorschlag = []; const preview = document.getElementById('schema-bild-preview'); preview.style.backgroundImage = ''; preview.classList.remove('hat-bild'); document.getElementById('schema-bild-input').value = ''; document.getElementById('schema-bild-entfernen-btn').style.display = 'none'; document.getElementById('schema-vorschlag').style.display = 'none'; document.getElementById('schema-farbe-eingabe').style.display = 'block'; // Farben zurücksetzen falls nur vom Vorschlag if (farben.length === 0) updateFarbeLabel(); }); }