1121 lines
33 KiB
Markdown
1121 lines
33 KiB
Markdown
# Farbhelfer Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Build a browser-based color tool that extracts hex codes from images, converts between color formats, shows color harmonies, and manages a personal color collection.
|
|
|
|
**Architecture:** Single `index.html` with tab-based navigation, three focused JS modules (`converter.js`, `picker.js`, `collection.js`), and one `style.css`. A global `state` object holds the active color and is passed between modules. No build step — open `index.html` directly in the browser.
|
|
|
|
**Tech Stack:** Vanilla HTML5, CSS3, JavaScript (ES6 modules), Canvas API, localStorage, no external dependencies.
|
|
|
|
---
|
|
|
|
## File Map
|
|
|
|
| File | Verantwortung |
|
|
|---|---|
|
|
| `index.html` | Shell, Tab-Navigation, alle Tab-Inhalte als Sections |
|
|
| `style.css` | Layout, Tab-UI, Farbfelder, Buttons, responsive Basis |
|
|
| `js/converter.js` | Hex-RGB-HSL Umrechnung, Harmonieberechnungen |
|
|
| `js/picker.js` | Bild laden (Upload/Paste/Drop), Canvas, Pixelauswahl |
|
|
| `js/collection.js` | localStorage lesen/schreiben, Favoriten, Historie, Schemata, Export/Import |
|
|
| `js/eingabe.js` | Eingabe-Tab Logik, bidirektionale Umrechnung |
|
|
| `js/harmonien.js` | Harmonien-Tab Logik, Rendering |
|
|
| `js/app.js` | Globaler State, Tab-Wechsel, Modul-Koordination |
|
|
|
|
---
|
|
|
|
### Task 1: Projekt-Grundstruktur
|
|
|
|
**Files:**
|
|
- Create: `index.html`
|
|
- Create: `style.css`
|
|
- Create: `js/app.js`
|
|
|
|
- [ ] **Schritt 1: `index.html` erstellen**
|
|
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Farbhelfer</title>
|
|
<link rel="stylesheet" href="style.css">
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>Farbhelfer</h1>
|
|
<nav>
|
|
<button class="tab-btn active" data-tab="picker">Picker</button>
|
|
<button class="tab-btn" data-tab="eingabe">Eingabe</button>
|
|
<button class="tab-btn" data-tab="harmonien">Harmonien</button>
|
|
<button class="tab-btn" data-tab="sammlung">Sammlung</button>
|
|
</nav>
|
|
</header>
|
|
|
|
<main>
|
|
<section id="tab-picker" class="tab-content active">
|
|
<p>Picker kommt hier</p>
|
|
</section>
|
|
<section id="tab-eingabe" class="tab-content">
|
|
<p>Eingabe kommt hier</p>
|
|
</section>
|
|
<section id="tab-harmonien" class="tab-content">
|
|
<p>Harmonien kommen hier</p>
|
|
</section>
|
|
<section id="tab-sammlung" class="tab-content">
|
|
<p>Sammlung kommt hier</p>
|
|
</section>
|
|
</main>
|
|
|
|
<script type="module" src="js/app.js"></script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
- [ ] **Schritt 2: `style.css` Basis erstellen**
|
|
|
|
```css
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
body {
|
|
font-family: system-ui, sans-serif;
|
|
background: #f5f5f5;
|
|
color: #222;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
header {
|
|
background: #fff;
|
|
border-bottom: 1px solid #ddd;
|
|
padding: 1rem 1.5rem;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 10;
|
|
}
|
|
|
|
header h1 { font-size: 1.2rem; margin-bottom: 0.75rem; }
|
|
|
|
nav { display: flex; gap: 0.5rem; }
|
|
|
|
.tab-btn {
|
|
padding: 0.4rem 1rem;
|
|
border: 1px solid #ccc;
|
|
border-radius: 6px;
|
|
background: #fff;
|
|
cursor: pointer;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.tab-btn.active {
|
|
background: #222;
|
|
color: #fff;
|
|
border-color: #222;
|
|
}
|
|
|
|
main { padding: 1.5rem; max-width: 900px; margin: 0 auto; }
|
|
|
|
.tab-content { display: none; }
|
|
.tab-content.active { display: block; }
|
|
|
|
.color-preview {
|
|
width: 100%;
|
|
height: 80px;
|
|
border-radius: 8px;
|
|
border: 1px solid #ddd;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.color-codes {
|
|
display: flex;
|
|
gap: 1rem;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.color-code-group { display: flex; flex-direction: column; gap: 0.25rem; }
|
|
.color-code-group label { font-size: 0.75rem; color: #666; text-transform: uppercase; }
|
|
.color-code-group input {
|
|
padding: 0.4rem 0.6rem;
|
|
border: 1px solid #ccc;
|
|
border-radius: 6px;
|
|
font-family: monospace;
|
|
font-size: 0.9rem;
|
|
width: 140px;
|
|
}
|
|
|
|
button.action-btn {
|
|
padding: 0.4rem 0.9rem;
|
|
border: 1px solid #ccc;
|
|
border-radius: 6px;
|
|
background: #fff;
|
|
cursor: pointer;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
button.action-btn:hover { background: #f0f0f0; }
|
|
|
|
.swatch {
|
|
display: inline-block;
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 6px;
|
|
border: 1px solid #ddd;
|
|
cursor: pointer;
|
|
vertical-align: middle;
|
|
}
|
|
```
|
|
|
|
- [ ] **Schritt 3: `js/app.js` mit Tab-Logik und globalem State erstellen**
|
|
|
|
```js
|
|
// Globaler State — aktive Farbe als { h, s, l } (HSL, 0-360, 0-100, 0-100)
|
|
export const state = {
|
|
color: { h: 200, s: 60, l: 50 },
|
|
setColor(hsl) {
|
|
this.color = hsl;
|
|
document.dispatchEvent(new CustomEvent('colorChanged', { detail: hsl }));
|
|
}
|
|
};
|
|
|
|
// Tab-Navigation
|
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const tab = btn.dataset.tab;
|
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
document.querySelectorAll('.tab-content').forEach(s => s.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
document.getElementById('tab-' + tab).classList.add('active');
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Schritt 4: Im Browser öffnen — Tab-Wechsel muss funktionieren**
|
|
|
|
`index.html` direkt im Browser öffnen (Doppelklick oder `open index.html`). Alle 4 Tabs müssen klickbar sein und den Inhalt wechseln.
|
|
|
|
- [ ] **Schritt 5: Committen**
|
|
|
|
```bash
|
|
git add index.html style.css js/app.js
|
|
git commit -m "feat: Grundstruktur mit Tab-Navigation"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: `converter.js` — Farbumrechnung und Harmonien
|
|
|
|
**Files:**
|
|
- Create: `js/converter.js`
|
|
|
|
- [ ] **Schritt 1: `js/converter.js` erstellen**
|
|
|
|
```js
|
|
// Hex → RGB
|
|
export function hexToRgb(hex) {
|
|
const h = hex.replace('#', '');
|
|
const n = parseInt(h.length === 3
|
|
? h.split('').map(c => c + c).join('')
|
|
: h, 16);
|
|
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
|
|
}
|
|
|
|
// RGB → Hex
|
|
export function rgbToHex({ r, g, b }) {
|
|
return '#' + [r, g, b].map(v => v.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
|
|
// RGB → HSL (h: 0-360, s: 0-100, l: 0-100)
|
|
export function rgbToHsl({ r, g, b }) {
|
|
r /= 255; g /= 255; b /= 255;
|
|
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
let h, s;
|
|
const l = (max + min) / 2;
|
|
if (max === min) {
|
|
h = s = 0;
|
|
} else {
|
|
const d = max - min;
|
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
switch (max) {
|
|
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
|
|
case g: h = ((b - r) / d + 2) / 6; break;
|
|
case b: h = ((r - g) / d + 4) / 6; break;
|
|
}
|
|
}
|
|
return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
|
|
}
|
|
|
|
// HSL → RGB
|
|
export function hslToRgb({ h, s, l }) {
|
|
s /= 100; l /= 100;
|
|
const k = n => (n + h / 30) % 12;
|
|
const a = s * Math.min(l, 1 - l);
|
|
const f = n => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
|
return { r: Math.round(f(0) * 255), g: Math.round(f(8) * 255), b: Math.round(f(4) * 255) };
|
|
}
|
|
|
|
// HSL → Hex
|
|
export function hslToHex(hsl) {
|
|
return rgbToHex(hslToRgb(hsl));
|
|
}
|
|
|
|
// Hex → HSL
|
|
export function hexToHsl(hex) {
|
|
return rgbToHsl(hexToRgb(hex));
|
|
}
|
|
|
|
// Harmonien — Input: HSL, Output: Objekt mit Arrays von HSL-Objekten
|
|
export function getHarmonies(hsl) {
|
|
const rotate = deg => ({ ...hsl, h: (hsl.h + deg + 360) % 360 });
|
|
return {
|
|
komplementaer: [rotate(180)],
|
|
analog: [rotate(-30), rotate(30)],
|
|
triade: [rotate(120), rotate(240)],
|
|
splitKomplementaer: [rotate(150), rotate(210)],
|
|
};
|
|
}
|
|
```
|
|
|
|
- [ ] **Schritt 2: Schnelltest in der Browser-Konsole**
|
|
|
|
`index.html` öffnen, in der Browserkonsole eingeben:
|
|
```js
|
|
import('./js/converter.js').then(m => {
|
|
console.log(m.hexToRgb('#3a8fc1')); // { r: 58, g: 143, b: 193 }
|
|
console.log(m.rgbToHex({r:58,g:143,b:193})); // #3a8fc1
|
|
console.log(m.rgbToHsl({r:58,g:143,b:193})); // { h: 204, s: 54, l: 49 }
|
|
console.log(m.hslToHex({h:204,s:54,l:49})); // nahe #3a8fc1
|
|
});
|
|
```
|
|
Alle Werte müssen stimmen.
|
|
|
|
- [ ] **Schritt 3: Committen**
|
|
|
|
```bash
|
|
git add js/converter.js
|
|
git commit -m "feat: Farbumrechnung Hex/RGB/HSL und Harmonieberechnungen"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3: Eingabe-Tab
|
|
|
|
**Files:**
|
|
- Modify: `index.html` — Tab-Inhalt für Eingabe befüllen
|
|
- Create: `js/eingabe.js`
|
|
- Modify: `js/app.js` — Eingabe-Modul initialisieren
|
|
|
|
- [ ] **Schritt 1: HTML für Eingabe-Tab in `index.html` ersetzen**
|
|
|
|
Den Platzhalter `<section id="tab-eingabe">` ersetzen mit:
|
|
|
|
```html
|
|
<section id="tab-eingabe" class="tab-content">
|
|
<h2>Farbe eingeben</h2>
|
|
<div id="eingabe-preview" class="color-preview" style="background:#3a8fc1"></div>
|
|
<div class="color-codes">
|
|
<div class="color-code-group">
|
|
<label>Hex</label>
|
|
<input id="eingabe-hex" type="text" value="#3a8fc1" maxlength="7">
|
|
</div>
|
|
<div class="color-code-group">
|
|
<label>RGB</label>
|
|
<input id="eingabe-rgb" type="text" value="58, 143, 193">
|
|
</div>
|
|
<div class="color-code-group">
|
|
<label>HSL</label>
|
|
<input id="eingabe-hsl" type="text" value="204, 54%, 49%">
|
|
</div>
|
|
</div>
|
|
<div style="display:flex;gap:0.5rem">
|
|
<button class="action-btn" id="eingabe-fav-btn">Zu Favoriten</button>
|
|
<button class="action-btn" id="eingabe-schema-btn">Zu Schema hinzufügen</button>
|
|
</div>
|
|
</section>
|
|
```
|
|
|
|
- [ ] **Schritt 2: `js/eingabe.js` erstellen**
|
|
|
|
```js
|
|
import { hexToRgb, rgbToHex, hexToHsl, hslToHex, hslToRgb, rgbToHsl } from './converter.js';
|
|
import { state } from './app.js';
|
|
|
|
function hslToDisplay({ h, s, l }) { return h + ', ' + s + '%, ' + l + '%'; }
|
|
function rgbToDisplay({ r, g, b }) { return r + ', ' + g + ', ' + b; }
|
|
|
|
function parseRgb(str) {
|
|
const parts = str.replace(/[^\d,]/g, '').split(',').map(Number);
|
|
if (parts.length !== 3 || parts.some(isNaN)) return null;
|
|
const [r, g, b] = parts;
|
|
if ([r, g, b].some(v => v < 0 || v > 255)) return null;
|
|
return { r, g, b };
|
|
}
|
|
|
|
function parseHsl(str) {
|
|
const parts = str.replace(/[^\d,]/g, '').split(',').map(Number);
|
|
if (parts.length !== 3 || parts.some(isNaN)) return null;
|
|
const [h, s, l] = parts;
|
|
if (h < 0 || h > 360 || s < 0 || s > 100 || l < 0 || l > 100) return null;
|
|
return { h, s, l };
|
|
}
|
|
|
|
function updateUI(hsl) {
|
|
const hex = hslToHex(hsl);
|
|
const rgb = hslToRgb(hsl);
|
|
document.getElementById('eingabe-preview').style.background = hex;
|
|
document.getElementById('eingabe-hex').value = hex;
|
|
document.getElementById('eingabe-rgb').value = rgbToDisplay(rgb);
|
|
document.getElementById('eingabe-hsl').value = hslToDisplay(hsl);
|
|
}
|
|
|
|
export function initEingabe(onSaveFavorit, onSaveSchema) {
|
|
const hexInput = document.getElementById('eingabe-hex');
|
|
const rgbInput = document.getElementById('eingabe-rgb');
|
|
const hslInput = document.getElementById('eingabe-hsl');
|
|
|
|
hexInput.addEventListener('input', () => {
|
|
const val = hexInput.value.trim();
|
|
if (/^#[0-9a-fA-F]{6}$/.test(val)) {
|
|
const hsl = hexToHsl(val);
|
|
state.setColor(hsl);
|
|
updateUI(hsl);
|
|
}
|
|
});
|
|
|
|
rgbInput.addEventListener('input', () => {
|
|
const rgb = parseRgb(rgbInput.value);
|
|
if (rgb) {
|
|
const hsl = rgbToHsl(rgb);
|
|
state.setColor(hsl);
|
|
updateUI(hsl);
|
|
}
|
|
});
|
|
|
|
hslInput.addEventListener('input', () => {
|
|
const hsl = parseHsl(hslInput.value);
|
|
if (hsl) {
|
|
state.setColor(hsl);
|
|
updateUI(hsl);
|
|
}
|
|
});
|
|
|
|
document.addEventListener('colorChanged', () => updateUI(state.color));
|
|
|
|
document.getElementById('eingabe-fav-btn').addEventListener('click', () => {
|
|
onSaveFavorit(state.color);
|
|
});
|
|
|
|
document.getElementById('eingabe-schema-btn').addEventListener('click', () => {
|
|
onSaveSchema(state.color);
|
|
});
|
|
|
|
updateUI(state.color);
|
|
}
|
|
```
|
|
|
|
- [ ] **Schritt 3: `initEingabe` in `app.js` einbinden**
|
|
|
|
Am Ende von `js/app.js` ergänzen (Platzhalter-Funktionen, bis `collection.js` in Task 6 fertig ist):
|
|
|
|
```js
|
|
import { initEingabe } from './eingabe.js';
|
|
|
|
function addFavorit(hsl) { console.log('addFavorit', hsl); }
|
|
function addColorToSchema(hsl) { console.log('addColorToSchema', hsl); }
|
|
|
|
initEingabe(addFavorit, addColorToSchema);
|
|
```
|
|
|
|
- [ ] **Schritt 4: Im Browser testen**
|
|
|
|
Eingabe-Tab öffnen:
|
|
- Hex `#ff6600` eingeben → RGB `255, 102, 0` und HSL `24, 100%, 50%` erscheinen, Preview wird orange
|
|
- RGB `0, 128, 0` eingeben → Hex `#008000`, HSL `120, 100%, 25%`
|
|
- HSL `300, 100%, 50%` eingeben → Hex `#ff00ff`, RGB `255, 0, 255`
|
|
|
|
- [ ] **Schritt 5: Committen**
|
|
|
|
```bash
|
|
git add index.html js/eingabe.js js/app.js
|
|
git commit -m "feat: Eingabe-Tab mit bidirektionaler Farbumrechnung"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 4: Picker-Tab
|
|
|
|
**Files:**
|
|
- Modify: `index.html` — Picker-Tab-Inhalt
|
|
- Create: `js/picker.js`
|
|
- Modify: `js/app.js` — Picker initialisieren
|
|
|
|
- [ ] **Schritt 1: HTML für Picker-Tab in `index.html` ersetzen**
|
|
|
|
Den Platzhalter `<section id="tab-picker">` ersetzen mit:
|
|
|
|
```html
|
|
<section id="tab-picker" class="tab-content active">
|
|
<h2>Farbe aus Bild</h2>
|
|
<div id="picker-dropzone">
|
|
<p>Bild hierher ziehen, einfügen (Strg+V) oder</p>
|
|
<button class="action-btn" id="picker-file-trigger">Datei wählen</button>
|
|
<input type="file" id="picker-file-input" accept="image/*" style="display:none">
|
|
</div>
|
|
<canvas id="picker-canvas" style="display:none;max-width:100%;cursor:crosshair;border-radius:8px;border:1px solid #ddd"></canvas>
|
|
<div id="picker-result" style="display:none;margin-top:1rem">
|
|
<div id="picker-preview" class="color-preview"></div>
|
|
<div class="color-codes">
|
|
<div class="color-code-group">
|
|
<label>Hex</label>
|
|
<input id="picker-hex" type="text" readonly>
|
|
</div>
|
|
<div class="color-code-group">
|
|
<label>RGB</label>
|
|
<input id="picker-rgb" type="text" readonly>
|
|
</div>
|
|
<div class="color-code-group">
|
|
<label>HSL</label>
|
|
<input id="picker-hsl" type="text" readonly>
|
|
</div>
|
|
</div>
|
|
<div style="display:flex;gap:0.5rem">
|
|
<button class="action-btn" id="picker-fav-btn">Zu Favoriten</button>
|
|
<button class="action-btn" id="picker-schema-btn">Zu Schema hinzufügen</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
```
|
|
|
|
CSS für Dropzone in `style.css` ergänzen:
|
|
|
|
```css
|
|
#picker-dropzone {
|
|
border: 2px dashed #ccc;
|
|
border-radius: 8px;
|
|
padding: 2rem;
|
|
text-align: center;
|
|
margin-bottom: 1rem;
|
|
background: #fafafa;
|
|
}
|
|
|
|
#picker-dropzone.drag-over {
|
|
border-color: #222;
|
|
background: #f0f0f0;
|
|
}
|
|
|
|
#picker-dropzone p { margin-bottom: 0.75rem; color: #666; font-size: 0.9rem; }
|
|
```
|
|
|
|
- [ ] **Schritt 2: `js/picker.js` erstellen**
|
|
|
|
```js
|
|
import { rgbToHex, rgbToHsl } from './converter.js';
|
|
import { state } from './app.js';
|
|
|
|
function rgbToDisplay({ r, g, b }) { return r + ', ' + g + ', ' + b; }
|
|
function hslToDisplay({ h, s, l }) { return h + ', ' + s + '%, ' + l + '%'; }
|
|
|
|
function showColor(r, g, b) {
|
|
const hex = rgbToHex({ r, g, b });
|
|
const hsl = rgbToHsl({ r, g, b });
|
|
state.setColor(hsl);
|
|
document.getElementById('picker-preview').style.background = hex;
|
|
document.getElementById('picker-hex').value = hex;
|
|
document.getElementById('picker-rgb').value = rgbToDisplay({ r, g, b });
|
|
document.getElementById('picker-hsl').value = hslToDisplay(hsl);
|
|
document.getElementById('picker-result').style.display = 'block';
|
|
}
|
|
|
|
function loadImage(file) {
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
const canvas = document.getElementById('picker-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 mx = Math.floor(img.width / 2);
|
|
const my = Math.floor(img.height / 2);
|
|
const px = ctx.getImageData(mx, my, 1, 1).data;
|
|
showColor(px[0], px[1], px[2]);
|
|
|
|
// Eyedropper: Klick wählt Pixel frei
|
|
canvas.onclick = (ev) => {
|
|
const rect = canvas.getBoundingClientRect();
|
|
const scaleX = canvas.width / rect.width;
|
|
const scaleY = canvas.height / rect.height;
|
|
const x = Math.floor((ev.clientX - rect.left) * scaleX);
|
|
const y = Math.floor((ev.clientY - rect.top) * scaleY);
|
|
const d = ctx.getImageData(x, y, 1, 1).data;
|
|
showColor(d[0], d[1], d[2]);
|
|
};
|
|
};
|
|
img.src = e.target.result;
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
|
|
export function initPicker(onSaveFavorit, onSaveSchema) {
|
|
const dropzone = document.getElementById('picker-dropzone');
|
|
|
|
document.getElementById('picker-file-trigger').addEventListener('click', () => {
|
|
document.getElementById('picker-file-input').click();
|
|
});
|
|
|
|
document.getElementById('picker-file-input').addEventListener('change', (e) => {
|
|
if (e.target.files[0]) loadImage(e.target.files[0]);
|
|
});
|
|
|
|
dropzone.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
dropzone.classList.add('drag-over');
|
|
});
|
|
dropzone.addEventListener('dragleave', () => dropzone.classList.remove('drag-over'));
|
|
dropzone.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
dropzone.classList.remove('drag-over');
|
|
const file = e.dataTransfer.files[0];
|
|
if (file && file.type.startsWith('image/')) loadImage(file);
|
|
});
|
|
|
|
// Paste (Strg+V / Cmd+V)
|
|
document.addEventListener('paste', (e) => {
|
|
const item = Array.from(e.clipboardData.items).find(i => i.type.startsWith('image/'));
|
|
if (item) loadImage(item.getAsFile());
|
|
});
|
|
|
|
document.getElementById('picker-fav-btn').addEventListener('click', () => onSaveFavorit(state.color));
|
|
document.getElementById('picker-schema-btn').addEventListener('click', () => onSaveSchema(state.color));
|
|
}
|
|
```
|
|
|
|
- [ ] **Schritt 3: `initPicker` in `app.js` einbinden**
|
|
|
|
Ergänzen (nach den bestehenden Imports):
|
|
|
|
```js
|
|
import { initPicker } from './picker.js';
|
|
|
|
initPicker(addFavorit, addColorToSchema);
|
|
```
|
|
|
|
- [ ] **Schritt 4: Im Browser testen**
|
|
|
|
- Screenshot mit `Cmd+Shift+4` (Mac) erstellen → mit `Cmd+V` einfügen → Bild erscheint, mittlerer Pixel wird vorgeschlagen
|
|
- Auf beliebigen Punkt im Bild klicken → Farbe wechselt
|
|
- Datei hochladen über "Datei wählen" → funktioniert gleich
|
|
- Bild per Drag & Drop ins Dropzone-Feld ziehen → Bild lädt
|
|
|
|
- [ ] **Schritt 5: Committen**
|
|
|
|
```bash
|
|
git add index.html style.css js/picker.js js/app.js
|
|
git commit -m "feat: Picker-Tab mit Upload, Paste und Eyedropper"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5: Harmonien-Tab
|
|
|
|
**Files:**
|
|
- Modify: `index.html` — Harmonien-Tab-Inhalt
|
|
- Create: `js/harmonien.js`
|
|
- Modify: `js/app.js` — Harmonien initialisieren
|
|
|
|
- [ ] **Schritt 1: HTML für Harmonien-Tab in `index.html` ersetzen**
|
|
|
|
```html
|
|
<section id="tab-harmonien" class="tab-content">
|
|
<h2>Farbharmonien</h2>
|
|
<p class="subtitle">Ausgangsfarbe: <span id="harmonien-base-hex">#3a8fc1</span></p>
|
|
<div id="harmonien-base-preview" class="color-preview" style="height:50px"></div>
|
|
<div id="harmonien-grid" style="display:flex;flex-direction:column;gap:1.5rem;margin-top:1rem"></div>
|
|
</section>
|
|
```
|
|
|
|
CSS in `style.css` ergänzen:
|
|
|
|
```css
|
|
.subtitle { font-size: 0.85rem; color: #666; margin-bottom: 0.5rem; }
|
|
|
|
.harmony-row h3 { font-size: 0.9rem; margin-bottom: 0.5rem; }
|
|
.harmony-swatches { display: flex; gap: 0.75rem; flex-wrap: wrap; align-items: flex-start; }
|
|
.harmony-swatch {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
}
|
|
.harmony-swatch .swatch { width: 56px; height: 56px; }
|
|
.harmony-swatch span { font-size: 0.75rem; font-family: monospace; }
|
|
```
|
|
|
|
- [ ] **Schritt 2: `js/harmonien.js` erstellen**
|
|
|
|
```js
|
|
import { getHarmonies, hslToHex } from './converter.js';
|
|
import { state } from './app.js';
|
|
|
|
function renderHarmony(label, colors, onSaveFavorit, onSaveSchema) {
|
|
const row = document.createElement('div');
|
|
row.className = 'harmony-row';
|
|
|
|
const heading = document.createElement('h3');
|
|
heading.textContent = label;
|
|
row.appendChild(heading);
|
|
|
|
const swatchesDiv = document.createElement('div');
|
|
swatchesDiv.className = 'harmony-swatches';
|
|
|
|
colors.forEach(hsl => {
|
|
const hex = hslToHex(hsl);
|
|
|
|
const div = document.createElement('div');
|
|
div.className = 'harmony-swatch';
|
|
|
|
const swatch = document.createElement('div');
|
|
swatch.className = 'swatch';
|
|
swatch.style.background = hex;
|
|
swatch.title = hex;
|
|
|
|
const label = document.createElement('span');
|
|
label.textContent = hex;
|
|
|
|
const btnRow = document.createElement('div');
|
|
btnRow.style.cssText = 'display:flex;gap:0.25rem';
|
|
|
|
const favBtn = document.createElement('button');
|
|
favBtn.className = 'action-btn';
|
|
favBtn.textContent = 'Fav';
|
|
favBtn.style.fontSize = '0.7rem';
|
|
favBtn.addEventListener('click', () => onSaveFavorit(hsl));
|
|
|
|
const schemaBtn = document.createElement('button');
|
|
schemaBtn.className = 'action-btn';
|
|
schemaBtn.textContent = '+Schema';
|
|
schemaBtn.style.fontSize = '0.7rem';
|
|
schemaBtn.addEventListener('click', () => onSaveSchema(hsl));
|
|
|
|
btnRow.appendChild(favBtn);
|
|
btnRow.appendChild(schemaBtn);
|
|
|
|
div.appendChild(swatch);
|
|
div.appendChild(label);
|
|
div.appendChild(btnRow);
|
|
swatchesDiv.appendChild(div);
|
|
});
|
|
|
|
row.appendChild(swatchesDiv);
|
|
return row;
|
|
}
|
|
|
|
function render(onSaveFavorit, onSaveSchema) {
|
|
const hsl = state.color;
|
|
const hex = hslToHex(hsl);
|
|
|
|
document.getElementById('harmonien-base-hex').textContent = hex;
|
|
document.getElementById('harmonien-base-preview').style.background = hex;
|
|
|
|
const grid = document.getElementById('harmonien-grid');
|
|
grid.textContent = '';
|
|
|
|
const h = getHarmonies(hsl);
|
|
grid.appendChild(renderHarmony('Komplementar', h.komplementaer, onSaveFavorit, onSaveSchema));
|
|
grid.appendChild(renderHarmony('Analog', h.analog, onSaveFavorit, onSaveSchema));
|
|
grid.appendChild(renderHarmony('Triade', h.triade, onSaveFavorit, onSaveSchema));
|
|
grid.appendChild(renderHarmony('Split-Komplementar', h.splitKomplementaer, onSaveFavorit, onSaveSchema));
|
|
}
|
|
|
|
export function initHarmonien(onSaveFavorit, onSaveSchema) {
|
|
document.addEventListener('colorChanged', () => render(onSaveFavorit, onSaveSchema));
|
|
render(onSaveFavorit, onSaveSchema);
|
|
}
|
|
```
|
|
|
|
- [ ] **Schritt 3: `initHarmonien` in `app.js` einbinden**
|
|
|
|
```js
|
|
import { initHarmonien } from './harmonien.js';
|
|
|
|
initHarmonien(addFavorit, addColorToSchema);
|
|
```
|
|
|
|
- [ ] **Schritt 4: Im Browser testen**
|
|
|
|
Harmonien-Tab öffnen → 4 Sektionen (Komplementar, Analog, Triade, Split-Komplementar) mit Farbfeldern und Hex-Codes. Im Eingabe-Tab Farbe ändern → zurück zu Harmonien → Farben haben sich aktualisiert.
|
|
|
|
- [ ] **Schritt 5: Committen**
|
|
|
|
```bash
|
|
git add index.html style.css js/harmonien.js js/app.js
|
|
git commit -m "feat: Harmonien-Tab mit Komplementar, Analog, Triade, Split-Komplementar"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 6: `collection.js` — Sammlung, localStorage, Export/Import
|
|
|
|
**Files:**
|
|
- Create: `js/collection.js`
|
|
- Modify: `index.html` — Sammlung-Tab-Inhalt
|
|
- Modify: `js/app.js` — Platzhalter-Funktionen durch echte Imports ersetzen
|
|
|
|
- [ ] **Schritt 1: `js/collection.js` erstellen**
|
|
|
|
```js
|
|
import { hslToHex } from './converter.js';
|
|
|
|
const STORAGE_KEY = 'farbhelfer';
|
|
const HISTORY_MAX = 20;
|
|
|
|
function load() {
|
|
try {
|
|
return JSON.parse(localStorage.getItem(STORAGE_KEY)) || { favoriten: [], history: [], schemata: [] };
|
|
} catch {
|
|
return { favoriten: [], history: [], schemata: [] };
|
|
}
|
|
}
|
|
|
|
function save(data) {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
}
|
|
|
|
export function addFavorit(hsl) {
|
|
const data = load();
|
|
const hex = hslToHex(hsl);
|
|
if (!data.favoriten.some(f => hslToHex(f) === hex)) {
|
|
data.favoriten.push(hsl);
|
|
save(data);
|
|
renderSammlung();
|
|
}
|
|
}
|
|
|
|
export function removeFavorit(hex) {
|
|
const data = load();
|
|
data.favoriten = data.favoriten.filter(f => hslToHex(f) !== hex);
|
|
save(data);
|
|
renderSammlung();
|
|
}
|
|
|
|
export function addToHistory(hsl) {
|
|
const data = load();
|
|
const hex = hslToHex(hsl);
|
|
data.history = data.history.filter(h => hslToHex(h) !== hex);
|
|
data.history.unshift(hsl);
|
|
if (data.history.length > HISTORY_MAX) data.history = data.history.slice(0, HISTORY_MAX);
|
|
save(data);
|
|
}
|
|
|
|
export function addColorToSchema(hsl) {
|
|
const name = prompt('Schema-Name (leer = letztes Schema):');
|
|
const data = load();
|
|
if (name) {
|
|
const existing = data.schemata.find(s => s.name === name);
|
|
if (existing) {
|
|
if (existing.farben.length < 4) {
|
|
existing.farben.push(hsl);
|
|
save(data);
|
|
renderSammlung();
|
|
} else {
|
|
alert('Schema hat bereits 4 Farben.');
|
|
}
|
|
} else {
|
|
data.schemata.push({ name, farben: [hsl] });
|
|
save(data);
|
|
renderSammlung();
|
|
}
|
|
} else if (data.schemata.length > 0) {
|
|
const last = data.schemata[data.schemata.length - 1];
|
|
if (last.farben.length < 4) {
|
|
last.farben.push(hsl);
|
|
save(data);
|
|
renderSammlung();
|
|
} else {
|
|
alert('Letztes Schema hat bereits 4 Farben.');
|
|
}
|
|
} else {
|
|
alert('Kein Schema vorhanden. Bitte Name eingeben.');
|
|
}
|
|
}
|
|
|
|
export function deleteSchema(name) {
|
|
const data = load();
|
|
data.schemata = data.schemata.filter(s => s.name !== name);
|
|
save(data);
|
|
renderSammlung();
|
|
}
|
|
|
|
export function exportCollection() {
|
|
const data = load();
|
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
const a = document.createElement('a');
|
|
a.href = URL.createObjectURL(blob);
|
|
a.download = 'farbhelfer-sammlung.json';
|
|
a.click();
|
|
}
|
|
|
|
export function importCollection() {
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.accept = '.json';
|
|
input.onchange = (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
const reader = new FileReader();
|
|
reader.onload = (ev) => {
|
|
try {
|
|
const imported = JSON.parse(ev.target.result);
|
|
const data = load();
|
|
const existingFavHexes = new Set(data.favoriten.map(hslToHex));
|
|
(imported.favoriten || []).forEach(hsl => {
|
|
if (!existingFavHexes.has(hslToHex(hsl))) data.favoriten.push(hsl);
|
|
});
|
|
const existingSchemaNames = new Set(data.schemata.map(s => s.name));
|
|
(imported.schemata || []).forEach(s => {
|
|
if (!existingSchemaNames.has(s.name)) data.schemata.push(s);
|
|
});
|
|
save(data);
|
|
renderSammlung();
|
|
alert('Import erfolgreich.');
|
|
} catch {
|
|
alert('Ungultige Datei.');
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
};
|
|
input.click();
|
|
}
|
|
|
|
function makeSwatch(hsl) {
|
|
const hex = hslToHex(hsl);
|
|
|
|
const wrapper = document.createElement('div');
|
|
wrapper.style.cssText = 'display:inline-flex;flex-direction:column;align-items:center;gap:0.25rem';
|
|
|
|
const swatch = document.createElement('div');
|
|
swatch.className = 'swatch';
|
|
swatch.style.background = hex;
|
|
swatch.title = hex;
|
|
|
|
const label = document.createElement('span');
|
|
label.style.cssText = 'font-size:0.75rem;font-family:monospace';
|
|
label.textContent = hex;
|
|
|
|
wrapper.appendChild(swatch);
|
|
wrapper.appendChild(label);
|
|
return wrapper;
|
|
}
|
|
|
|
export function renderSammlung() {
|
|
const data = load();
|
|
|
|
// Favoriten
|
|
const favContainer = document.getElementById('sammlung-favoriten');
|
|
if (favContainer) {
|
|
favContainer.textContent = '';
|
|
if (data.favoriten.length === 0) {
|
|
const msg = document.createElement('p');
|
|
msg.style.cssText = 'color:#999;font-size:0.85rem';
|
|
msg.textContent = 'Noch keine Favoriten.';
|
|
favContainer.appendChild(msg);
|
|
} else {
|
|
data.favoriten.forEach(hsl => {
|
|
const hex = hslToHex(hsl);
|
|
const cell = document.createElement('div');
|
|
cell.style.cssText = 'display:inline-flex;flex-direction:column;align-items:center;gap:0.25rem;margin:0.25rem';
|
|
cell.appendChild(makeSwatch(hsl));
|
|
|
|
const del = document.createElement('button');
|
|
del.className = 'action-btn';
|
|
del.textContent = 'Loschen';
|
|
del.style.fontSize = '0.7rem';
|
|
del.addEventListener('click', () => removeFavorit(hex));
|
|
cell.appendChild(del);
|
|
favContainer.appendChild(cell);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Historie
|
|
const histContainer = document.getElementById('sammlung-history');
|
|
if (histContainer) {
|
|
histContainer.textContent = '';
|
|
if (data.history.length === 0) {
|
|
const msg = document.createElement('p');
|
|
msg.style.cssText = 'color:#999;font-size:0.85rem';
|
|
msg.textContent = 'Noch kein Verlauf.';
|
|
histContainer.appendChild(msg);
|
|
} else {
|
|
data.history.forEach(hsl => {
|
|
const cell = document.createElement('div');
|
|
cell.style.cssText = 'display:inline-block;margin:0.25rem';
|
|
cell.appendChild(makeSwatch(hsl));
|
|
histContainer.appendChild(cell);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Schemata
|
|
const schemataContainer = document.getElementById('sammlung-schemata');
|
|
if (schemataContainer) {
|
|
schemataContainer.textContent = '';
|
|
if (data.schemata.length === 0) {
|
|
const msg = document.createElement('p');
|
|
msg.style.cssText = 'color:#999;font-size:0.85rem';
|
|
msg.textContent = 'Noch keine Schemata.';
|
|
schemataContainer.appendChild(msg);
|
|
} else {
|
|
data.schemata.forEach(schema => {
|
|
const card = document.createElement('div');
|
|
card.style.cssText = 'border:1px solid #ddd;border-radius:8px;padding:1rem;margin-bottom:0.75rem;background:#fff';
|
|
|
|
const header = document.createElement('div');
|
|
header.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem';
|
|
|
|
const nameEl = document.createElement('strong');
|
|
nameEl.textContent = schema.name;
|
|
|
|
const delBtn = document.createElement('button');
|
|
delBtn.className = 'action-btn';
|
|
delBtn.textContent = 'Schema loschen';
|
|
delBtn.style.fontSize = '0.75rem';
|
|
delBtn.addEventListener('click', () => deleteSchema(schema.name));
|
|
|
|
header.appendChild(nameEl);
|
|
header.appendChild(delBtn);
|
|
|
|
const swatchesDiv = document.createElement('div');
|
|
swatchesDiv.style.cssText = 'display:flex;gap:0.5rem;flex-wrap:wrap';
|
|
schema.farben.forEach(hsl => swatchesDiv.appendChild(makeSwatch(hsl)));
|
|
|
|
card.appendChild(header);
|
|
card.appendChild(swatchesDiv);
|
|
schemataContainer.appendChild(card);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Schritt 2: HTML für Sammlung-Tab in `index.html` ersetzen**
|
|
|
|
```html
|
|
<section id="tab-sammlung" class="tab-content">
|
|
<h2>Sammlung</h2>
|
|
|
|
<div style="display:flex;gap:0.5rem;margin-bottom:1.5rem">
|
|
<button class="action-btn" id="sammlung-export-btn">Exportieren</button>
|
|
<button class="action-btn" id="sammlung-import-btn">Importieren</button>
|
|
</div>
|
|
|
|
<h3 style="margin-bottom:0.5rem">Favoriten</h3>
|
|
<div id="sammlung-favoriten" style="display:flex;flex-wrap:wrap;gap:0.5rem;margin-bottom:1.5rem"></div>
|
|
|
|
<h3 style="margin-bottom:0.5rem">Verlauf (letzte 20)</h3>
|
|
<div id="sammlung-history" style="display:flex;flex-wrap:wrap;gap:0.5rem;margin-bottom:1.5rem"></div>
|
|
|
|
<h3 style="margin-bottom:0.5rem">Farbschemata</h3>
|
|
<div id="sammlung-schemata"></div>
|
|
</section>
|
|
```
|
|
|
|
- [ ] **Schritt 3: `app.js` vollständig aktualisieren**
|
|
|
|
`js/app.js` komplett ersetzen mit der finalen Version (Platzhalter weg, alle echten Imports):
|
|
|
|
```js
|
|
import { addFavorit, addColorToSchema, addToHistory, renderSammlung, exportCollection, importCollection } from './collection.js';
|
|
import { initEingabe } from './eingabe.js';
|
|
import { initPicker } from './picker.js';
|
|
import { initHarmonien } from './harmonien.js';
|
|
|
|
export const state = {
|
|
color: { h: 200, s: 60, l: 50 },
|
|
setColor(hsl) {
|
|
this.color = hsl;
|
|
document.dispatchEvent(new CustomEvent('colorChanged', { detail: hsl }));
|
|
}
|
|
};
|
|
|
|
// Tab-Navigation
|
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const tab = btn.dataset.tab;
|
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
document.querySelectorAll('.tab-content').forEach(s => s.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
document.getElementById('tab-' + tab).classList.add('active');
|
|
});
|
|
});
|
|
|
|
// Jede Farbänderung in die Historie
|
|
document.addEventListener('colorChanged', (e) => addToHistory(e.detail));
|
|
|
|
initEingabe(addFavorit, addColorToSchema);
|
|
initPicker(addFavorit, addColorToSchema);
|
|
initHarmonien(addFavorit, addColorToSchema);
|
|
|
|
document.getElementById('sammlung-export-btn').addEventListener('click', exportCollection);
|
|
document.getElementById('sammlung-import-btn').addEventListener('click', importCollection);
|
|
|
|
renderSammlung();
|
|
```
|
|
|
|
- [ ] **Schritt 4: Im Browser testen**
|
|
|
|
- Im Eingabe-Tab Farbe eingeben → "Zu Favoriten" → Sammlung-Tab zeigt sie
|
|
- Im Picker Farbe wählen → Verlauf in Sammlung aktualisiert sich
|
|
- "Zu Schema hinzufügen" → Prompt erscheint → Schema-Name eingeben → Sammlung zeigt Schema
|
|
- Exportieren → JSON-Download startet
|
|
- Importierte JSON wieder importieren → keine Duplikate
|
|
|
|
- [ ] **Schritt 5: Committen**
|
|
|
|
```bash
|
|
git add js/collection.js index.html js/app.js
|
|
git commit -m "feat: Sammlung mit Favoriten, Verlauf, Schemata, Export und Import"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 7: Finales CSS-Finishing
|
|
|
|
**Files:**
|
|
- Modify: `style.css`
|
|
|
|
- [ ] **Schritt 1: Finale Styles an `style.css` anhängen**
|
|
|
|
```css
|
|
h2 { font-size: 1.1rem; margin-bottom: 1rem; }
|
|
h3 { font-size: 0.95rem; }
|
|
|
|
.tab-content { padding-top: 0.5rem; }
|
|
|
|
.color-preview { transition: background 0.15s; }
|
|
|
|
.action-btn:active { transform: scale(0.97); }
|
|
|
|
@media (max-width: 600px) {
|
|
nav { flex-wrap: wrap; }
|
|
.color-codes { flex-direction: column; }
|
|
.color-code-group input { width: 100%; }
|
|
}
|
|
```
|
|
|
|
- [ ] **Schritt 2: Visuell prüfen**
|
|
|
|
Alle 4 Tabs durchklicken — aufgeräumtes Layout? DevTools → Toggle device toolbar → unter 600px: Navigation und Inputs stapeln sich vertikal.
|
|
|
|
- [ ] **Schritt 3: Committen**
|
|
|
|
```bash
|
|
git add style.css
|
|
git commit -m "style: finales CSS-Finishing und mobile Anpassungen"
|
|
```
|