From 447473c6f57fd5982af044dd340f9ae86965cbda Mon Sep 17 00:00:00 2001 From: Ferdinand Date: Thu, 2 Apr 2026 14:23:26 +0200 Subject: [PATCH] feat: Modal zum manuellen Erstellen von Farbschemata --- index.html | 44 +++++++++++ js/app.js | 4 +- js/collection.js | 12 +++ js/schema-modal.js | 192 +++++++++++++++++++++++++++++++++++++++++++++ style.css | 58 ++++++++++++++ 5 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 js/schema-modal.js diff --git a/index.html b/index.html index 00638db..9cd67a7 100644 --- a/index.html +++ b/index.html @@ -91,10 +91,54 @@

Farbschemata

+
+ + + diff --git a/js/app.js b/js/app.js index 10ac70a..4cbd683 100644 --- a/js/app.js +++ b/js/app.js @@ -1,7 +1,8 @@ import { initEingabe } from './eingabe.js'; import { initPicker } from './picker.js'; import { initHarmonien } from './harmonien.js'; -import { addFavorit, addColorToSchema, addToHistory, renderSammlung, exportCollection, importCollection } from './collection.js'; +import { addFavorit, addColorToSchema, addToHistory, renderSammlung, exportCollection, importCollection, saveSchema } from './collection.js'; +import { initSchemaModal } from './schema-modal.js'; // state.color is read-only from outside — always use setColor() to update, // so that the colorChanged event is dispatched to all listening modules. @@ -37,3 +38,4 @@ document.getElementById('sammlung-export-btn').addEventListener('click', exportC document.getElementById('sammlung-import-btn').addEventListener('click', importCollection); renderSammlung(); +initSchemaModal(saveSchema); diff --git a/js/collection.js b/js/collection.js index 8de1ac1..fda00a6 100644 --- a/js/collection.js +++ b/js/collection.js @@ -74,6 +74,18 @@ export function addColorToSchema(hsl) { } } +export function saveSchema(name, farben) { + const data = load(); + const existing = data.schemata.find(s => s.name === name); + if (existing) { + existing.farben = farben; + } else { + data.schemata.push({ name, farben }); + } + save(data); + renderSammlung(); +} + export function deleteSchema(name) { const data = load(); data.schemata = data.schemata.filter(s => s.name !== name); diff --git a/js/schema-modal.js b/js/schema-modal.js new file mode 100644 index 0000000..767f842 --- /dev/null +++ b/js/schema-modal.js @@ -0,0 +1,192 @@ +import { hexToHsl, hslToHex, rgbToHsl, rgbToHex } from './converter.js'; + +const MAX_FARBEN = 4; + +// Aktuell gesammelter Zustand des Modals +let farben = []; // Array von HSL-Objekten +let aktuelleHex = ''; // Hex-Wert des aktuellen Eingabefelds + +function resetModal() { + farben = []; + aktuelleHex = ''; + 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-farbe-eingabe').style.display = 'block'; + document.getElementById('schema-abschliessen-btn').disabled = true; + updateFarbeLabel(); +} + +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 initSchemaModal(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; + + onSave(name, farben); + document.getElementById('schema-modal-overlay').style.display = 'none'; + }); +} diff --git a/style.css b/style.css index 53dba98..01acb01 100644 --- a/style.css +++ b/style.css @@ -167,6 +167,64 @@ button.action-btn:hover { background: #f0f0f0; } gap: 0.5rem; } +/* --- Modal --- */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.45); + z-index: 200; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; +} + +.modal { + background: #fff; + border-radius: 12px; + padding: 1.5rem; + width: 100%; + max-width: 460px; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 8px 32px rgba(0,0,0,0.18); +} + +.modal-field { + display: flex; + flex-direction: column; + gap: 0.3rem; + margin-bottom: 0.75rem; +} + +.modal-field label { font-size: 0.75rem; color: #666; text-transform: uppercase; } +.modal-field input { + padding: 0.4rem 0.6rem; + border: 1px solid #ccc; + border-radius: 6px; + font-size: 0.9rem; +} + +.schema-farben-preview { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + min-height: 20px; + margin-bottom: 1rem; +} + +.schema-dropzone { + border: 2px dashed #ccc; + border-radius: 8px; + padding: 1rem; + text-align: center; + background: #fafafa; + font-size: 0.85rem; + color: #666; +} + +.schema-dropzone p { margin-bottom: 0.5rem; } + /* --- Finale Styles --- */ h2 { font-size: 1.1rem; margin-bottom: 1rem; } h3 { font-size: 0.95rem; margin-bottom: 0.5rem; }