DevTools: Farb-Rechner mit HEX/RGB/RGBA/HSL/HSV/CMYK, Color Picker, Alpha, WCAG-Kontrast
This commit is contained in:
@@ -2360,6 +2360,325 @@ function JsonTool({ toast, mobile }) {
|
||||
|
||||
|
||||
|
||||
// ──────────────────────────────────────────────────────────── Farb-Rechner ──
|
||||
|
||||
// ── Konvertierungsfunktionen ──
|
||||
function hexToRgb(hex) {
|
||||
hex = hex.replace(/^#/, '');
|
||||
if (hex.length === 3) hex = hex.split('').map(c=>c+c).join('');
|
||||
if (hex.length === 4) hex = hex.split('').map(c=>c+c).join(''); // #rgba
|
||||
const n = parseInt(hex.slice(0,6), 16);
|
||||
const a = hex.length === 8 ? parseInt(hex.slice(6,8),16)/255 : 1;
|
||||
return { r:(n>>16)&255, g:(n>>8)&255, b:n&255, a };
|
||||
}
|
||||
|
||||
function rgbToHex({r,g,b,a=1}) {
|
||||
const h = [r,g,b].map(v=>Math.round(v).toString(16).padStart(2,'0')).join('');
|
||||
const ah = a < 1 ? Math.round(a*255).toString(16).padStart(2,'0') : '';
|
||||
return '#' + h + ah;
|
||||
}
|
||||
|
||||
function rgbToHsl({r,g,b,a=1}) {
|
||||
r/=255; g/=255; b/=255;
|
||||
const max=Math.max(r,g,b), min=Math.min(r,g,b);
|
||||
let h=0,s=0,l=(max+min)/2;
|
||||
if (max!==min) {
|
||||
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), a };
|
||||
}
|
||||
|
||||
function hslToRgb({h,s,l,a=1}) {
|
||||
s/=100; l/=100;
|
||||
const k=n=>(n+h/30)%12;
|
||||
const c=s*Math.min(l,1-l);
|
||||
const f=n=>l-c*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), a};
|
||||
}
|
||||
|
||||
function rgbToHsv({r,g,b,a=1}) {
|
||||
r/=255; g/=255; b/=255;
|
||||
const max=Math.max(r,g,b), min=Math.min(r,g,b), d=max-min;
|
||||
let h=0;
|
||||
if (d!==0){
|
||||
switch(max){
|
||||
case r: h=((g-b)/d)%6; break;
|
||||
case g: h=(b-r)/d+2; break;
|
||||
case b: h=(r-g)/d+4; break;
|
||||
}
|
||||
h=Math.round(h*60); if(h<0) h+=360;
|
||||
}
|
||||
return {h, s:max===0?0:Math.round(d/max*100), v:Math.round(max*100), a};
|
||||
}
|
||||
|
||||
function rgbToCmyk({r,g,b}) {
|
||||
r/=255; g/=255; b/=255;
|
||||
const k=1-Math.max(r,g,b);
|
||||
if (k===1) return {c:0,m:0,y:0,k:100};
|
||||
return {
|
||||
c:Math.round((1-r-k)/(1-k)*100),
|
||||
m:Math.round((1-g-k)/(1-k)*100),
|
||||
y:Math.round((1-b-k)/(1-k)*100),
|
||||
k:Math.round(k*100)
|
||||
};
|
||||
}
|
||||
|
||||
// Relative luminance (WCAG)
|
||||
function luminance({r,g,b}) {
|
||||
const c = [r,g,b].map(v => {
|
||||
v/=255;
|
||||
return v<=0.03928 ? v/12.92 : Math.pow((v+0.055)/1.055,2.4);
|
||||
});
|
||||
return 0.2126*c[0]+0.7152*c[1]+0.0722*c[2];
|
||||
}
|
||||
|
||||
function contrastRatio(lum1, lum2) {
|
||||
const [l,d] = lum1>lum2 ? [lum1,lum2] : [lum2,lum1];
|
||||
return (l+0.05)/(d+0.05);
|
||||
}
|
||||
|
||||
// Parse any color string input → {r,g,b,a} or null
|
||||
function parseColor(raw) {
|
||||
if (!raw) return null;
|
||||
raw = raw.trim();
|
||||
|
||||
// hex
|
||||
if (/^#?[0-9a-fA-F]{3,8}$/.test(raw)) {
|
||||
try { return hexToRgb(raw.startsWith('#')?raw:'#'+raw); } catch {}
|
||||
}
|
||||
|
||||
// rgb / rgba
|
||||
const rgbM = raw.match(/rgba?\(\s*([\d.]+)\s*[,\s]\s*([\d.]+)\s*[,\s]\s*([\d.]+)(?:\s*[,\/]\s*([\d.]+%?))?\s*\)/i);
|
||||
if (rgbM) {
|
||||
const a = rgbM[4] ? (rgbM[4].includes('%') ? parseFloat(rgbM[4])/100 : parseFloat(rgbM[4])) : 1;
|
||||
return {r:+rgbM[1],g:+rgbM[2],b:+rgbM[3],a};
|
||||
}
|
||||
|
||||
// hsl / hsla
|
||||
const hslM = raw.match(/hsla?\(\s*([\d.]+)\s*[,\s]\s*([\d.]+)%?\s*[,\s]\s*([\d.]+)%?(?:\s*[,\/]\s*([\d.]+%?))?\s*\)/i);
|
||||
if (hslM) {
|
||||
const a = hslM[4] ? (hslM[4].includes('%')?parseFloat(hslM[4])/100:parseFloat(hslM[4])) : 1;
|
||||
return hslToRgb({h:+hslM[1],s:+hslM[2],l:+hslM[3],a});
|
||||
}
|
||||
|
||||
// CSS color names (basic set)
|
||||
const NAMES = {red:[255,0,0],green:[0,128,0],blue:[0,0,255],white:[255,255,255],black:[0,0,0],
|
||||
yellow:[255,255,0],cyan:[0,255,255],magenta:[255,0,255],orange:[255,165,0],purple:[128,0,128],
|
||||
pink:[255,192,203],gray:[128,128,128],grey:[128,128,128],lime:[0,255,0],navy:[0,0,128],
|
||||
teal:[0,128,128],silver:[192,192,192],maroon:[128,0,0],olive:[128,128,0]};
|
||||
const name = NAMES[raw.toLowerCase()];
|
||||
if (name) return {r:name[0],g:name[1],b:name[2],a:1};
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function fmt2(n) { return Math.round(n*100)/100; }
|
||||
|
||||
function FarbRechner({ toast, mobile }) {
|
||||
const [input, setInput] = useState('');
|
||||
const [pickerHex,setPickerHex]= useState('#4ecdc4');
|
||||
const [color, setColor] = useState(null); // {r,g,b,a}
|
||||
const [copied, setCopied] = useState('');
|
||||
|
||||
function applyColor(rgba) {
|
||||
setColor(rgba);
|
||||
// sync picker (ignore alpha for picker)
|
||||
setPickerHex(rgbToHex({...rgba,a:1}));
|
||||
}
|
||||
|
||||
function handleInput(val) {
|
||||
setInput(val);
|
||||
const c = parseColor(val);
|
||||
if (c) applyColor(c);
|
||||
else if (!val.trim()) setColor(null);
|
||||
}
|
||||
|
||||
function handlePicker(hex) {
|
||||
setPickerHex(hex);
|
||||
const c = hexToRgb(hex);
|
||||
// preserve alpha from current color
|
||||
const a = color?.a ?? 1;
|
||||
setInput(hex);
|
||||
applyColor({...c, a});
|
||||
setInput(hex);
|
||||
}
|
||||
|
||||
async function copy(text, key) {
|
||||
try { await navigator.clipboard.writeText(text); setCopied(key); setTimeout(()=>setCopied(''),1800); }
|
||||
catch { toast('Kopieren fehlgeschlagen'); }
|
||||
}
|
||||
|
||||
const inp = {...S.inp, fontSize:13, padding:'7px 9px', boxSizing:'border-box'};
|
||||
|
||||
// Derived formats
|
||||
const formats = color ? (() => {
|
||||
const {r,g,b,a} = color;
|
||||
const hsl = rgbToHsl(color);
|
||||
const hsv = rgbToHsv(color);
|
||||
const cmyk = rgbToCmyk(color);
|
||||
const hex6 = rgbToHex({r,g,b,a:1});
|
||||
const hex8 = rgbToHex({r,g,b,a});
|
||||
const hasAlpha = a < 1;
|
||||
|
||||
const lum = luminance({r,g,b});
|
||||
const contrastWhite = fmt2(contrastRatio(lum, 1));
|
||||
const contrastBlack = fmt2(contrastRatio(lum, 0));
|
||||
const wcagWhite = contrastWhite>=7?'AAA':contrastWhite>=4.5?'AA':contrastWhite>=3?'AA (groß)':'✗';
|
||||
const wcagBlack = contrastBlack>=7?'AAA':contrastBlack>=4.5?'AA':contrastBlack>=3?'AA (groß)':'✗';
|
||||
const textColor = lum > 0.179 ? '#000' : '#fff';
|
||||
|
||||
return [
|
||||
{key:'hex6', label:'HEX', value: hex6, copy: hex6},
|
||||
{key:'hex8', label:'HEX + Alpha', value: hex8, copy: hex8, hide: !hasAlpha},
|
||||
{key:'rgb', label:'RGB', value: `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})`, copy: `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})`},
|
||||
{key:'rgba', label:'RGBA', value: `rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, ${fmt2(a)})`, copy: `rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, ${fmt2(a)})`},
|
||||
{key:'hsl', label:'HSL', value: `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`, copy: `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`},
|
||||
{key:'hsla', label:'HSLA', value: `hsla(${hsl.h}, ${hsl.s}%, ${hsl.l}%, ${fmt2(a)})`, copy: `hsla(${hsl.h}, ${hsl.s}%, ${hsl.l}%, ${fmt2(a)})`},
|
||||
{key:'hsv', label:'HSV / HSB', value: `hsv(${hsv.h}°, ${hsv.s}%, ${hsv.v}%)`, copy: `hsv(${hsv.h}, ${hsv.s}%, ${hsv.v}%)`},
|
||||
{key:'cmyk', label:'CMYK', value: `cmyk(${cmyk.c}%, ${cmyk.m}%, ${cmyk.y}%, ${cmyk.k}%)`, copy: `cmyk(${cmyk.c}%, ${cmyk.m}%, ${cmyk.y}%, ${cmyk.k}%)`},
|
||||
{key:'css', label:'CSS custom property', value: `--color: ${hex6};`, copy: `--color: ${hex6};`},
|
||||
{key:'r', label:'R (0–255)', value: String(Math.round(r)), copy: String(Math.round(r)), muted:true},
|
||||
{key:'g', label:'G (0–255)', value: String(Math.round(g)), copy: String(Math.round(g)), muted:true},
|
||||
{key:'b', label:'B (0–255)', value: String(Math.round(b)), copy: String(Math.round(b)), muted:true},
|
||||
{key:'a', label:'Alpha (0–1)', value: fmt2(a), copy: String(fmt2(a)), muted:true},
|
||||
{key:'lum', label:'Relative Luminanz', value: fmt2(lum), copy: String(fmt2(lum)), muted:true},
|
||||
{key:'cw', label:'Kontrast zu Weiß', value: `${contrastWhite}:1 — WCAG: ${wcagWhite}`, copy: `${contrastWhite}:1`, muted:true},
|
||||
{key:'cb', label:'Kontrast zu Schwarz', value: `${contrastBlack}:1 — WCAG: ${wcagBlack}`, copy: `${contrastBlack}:1`, muted:true},
|
||||
].filter(f=>!f.hide);
|
||||
})() : [];
|
||||
|
||||
const previewBg = color ? `rgb(${Math.round(color.r)},${Math.round(color.g)},${Math.round(color.b)})` : 'rgba(255,255,255,0.05)';
|
||||
const previewText = color ? (luminance(color) > 0.179 ? '#000' : '#fff') : 'rgba(255,255,255,0.3)';
|
||||
|
||||
// Gradient bar stops for hue slider background
|
||||
const hueBar = 'linear-gradient(to right,#f00,#ff0,#0f0,#0ff,#00f,#f0f,#f00)';
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Eingabe-Zeile */}
|
||||
<div style={{display:'flex', gap:10, alignItems:'center', marginBottom:16, flexWrap:'wrap'}}>
|
||||
<div style={{flex:1, minWidth:160}}>
|
||||
<div style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:10,marginBottom:4}}>
|
||||
Farbe eingeben (HEX, RGB, HSL, Farbname…)
|
||||
</div>
|
||||
<input style={{...inp, width:'100%',
|
||||
borderColor: input && !color ? 'rgba(255,107,157,0.5)' : undefined}}
|
||||
placeholder="#4ecdc4 oder rgb(78,205,196) oder hsl(177,55%,55%)"
|
||||
value={input} onChange={e=>handleInput(e.target.value)}/>
|
||||
</div>
|
||||
{/* Color Picker */}
|
||||
<div>
|
||||
<div style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:10,marginBottom:4}}>
|
||||
Color Picker
|
||||
</div>
|
||||
<div style={{position:'relative', width:42, height:36}}>
|
||||
<div style={{width:42,height:36,borderRadius:7,background:pickerHex,
|
||||
border:'2px solid rgba(255,255,255,0.15)',cursor:'pointer',overflow:'hidden'}}>
|
||||
<input type="color" value={pickerHex}
|
||||
onChange={e=>handlePicker(e.target.value)}
|
||||
style={{opacity:0,position:'absolute',inset:0,width:'100%',height:'100%',cursor:'pointer',border:'none',padding:0}}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Alpha Slider */}
|
||||
{!!color && (
|
||||
<div style={{flex:'0 0 140px'}}>
|
||||
<div style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:10,marginBottom:4}}>
|
||||
Alpha / Transparenz: {Math.round((color.a??1)*100)}%
|
||||
</div>
|
||||
<input type="range" min={0} max={1} step={0.01}
|
||||
value={color?.a??1}
|
||||
onChange={e=>applyColor({...color, a:parseFloat(e.target.value)})}
|
||||
style={{width:'100%', accentColor:'#4ecdc4'}}/>
|
||||
</div>
|
||||
)}
|
||||
{!!input && <button style={{...S.btn('#ff6b9d',false),fontSize:11,padding:'7px 14px'}}
|
||||
onClick={()=>{setInput('');setColor(null);}}>✕ Leeren</button>}
|
||||
</div>
|
||||
|
||||
{/* Vorschau */}
|
||||
{color && (
|
||||
<div style={{borderRadius:10,overflow:'hidden',marginBottom:16,border:'1px solid rgba(255,255,255,0.08)'}}>
|
||||
{/* Checkerboard + Farbe */}
|
||||
<div style={{
|
||||
height:80,
|
||||
background:`linear-gradient(${previewBg},${previewBg}),
|
||||
repeating-conic-gradient(rgba(255,255,255,0.1) 0% 25%, transparent 0% 50%) 0 0 / 16px 16px`,
|
||||
display:'flex',alignItems:'center',justifyContent:'center'}}>
|
||||
<div style={{
|
||||
background: color.a < 1
|
||||
? `rgba(${Math.round(color.r)},${Math.round(color.g)},${Math.round(color.b)},${color.a})`
|
||||
: previewBg,
|
||||
width:'100%', height:'100%', display:'flex',alignItems:'center',justifyContent:'center'}}>
|
||||
<span style={{color:previewText, fontFamily:'monospace', fontSize:13, fontWeight:700,
|
||||
textShadow: luminance(color)>0.5?'0 1px 2px rgba(0,0,0,0.2)':'0 1px 2px rgba(0,0,0,0.5)'}}>
|
||||
{rgbToHex({...color,a:1}).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Komplementärfarbe Vorschau */}
|
||||
<div style={{display:'flex',height:32}}>
|
||||
{[0,30,60,90,120,150,180,210,240,270,300,330].map(dh=>{
|
||||
const hsl = rgbToHsl(color);
|
||||
const c2 = hslToRgb({...hsl, h:(hsl.h+dh)%360, a:1});
|
||||
return <div key={dh} style={{flex:1,background:`rgb(${c2.r},${c2.g},${c2.b})`}} title={`+${dh}°`}/>;
|
||||
})}
|
||||
</div>
|
||||
<div style={{padding:'4px 10px',background:'rgba(0,0,0,0.2)',
|
||||
color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:9}}>
|
||||
Farbrad-Vorschau (Töne in 30°-Schritten)
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Format-Ausgaben */}
|
||||
{!!formats.length && (
|
||||
<div style={{display:'flex',flexDirection:'column',gap:4}}>
|
||||
{formats.map(f=>(
|
||||
<div key={f.key} onClick={()=>copy(f.copy, f.key)}
|
||||
style={{display:'flex',justifyContent:'space-between',alignItems:'center',
|
||||
padding:'7px 10px',borderRadius:7,cursor:'pointer',userSelect:'none',
|
||||
background:'rgba(255,255,255,0.03)',border:'1px solid rgba(255,255,255,0.05)',
|
||||
transition:'background 0.15s'}}
|
||||
onMouseEnter={e=>e.currentTarget.style.background='rgba(255,255,255,0.07)'}
|
||||
onMouseLeave={e=>e.currentTarget.style.background='rgba(255,255,255,0.03)'}>
|
||||
<span style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:11,minWidth:120}}>{f.label}</span>
|
||||
<code style={{color: f.muted?'rgba(255,255,255,0.55)':'#4ecdc4',
|
||||
fontFamily:"'Courier New',monospace",fontSize:12,fontWeight:700,
|
||||
flex:1,textAlign:'right',marginLeft:8,wordBreak:'break-all'}}>
|
||||
{f.value}
|
||||
</code>
|
||||
<span style={{color:copied===f.key?'#4ecdc4':'rgba(255,255,255,0.2)',
|
||||
fontSize:14,marginLeft:10,flexShrink:0,transition:'color 0.2s'}}>
|
||||
{copied===f.key?'✓':'⎘'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!color && !input && (
|
||||
<div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:11,
|
||||
padding:'20px 0',textAlign:'center'}}>
|
||||
Farbe eingeben oder im Color Picker auswählen → alle Formate werden berechnet
|
||||
</div>
|
||||
)}
|
||||
{!!input && !color && (
|
||||
<div style={{color:'#ff6b9d',fontFamily:'monospace',fontSize:11,padding:'8px 0'}}>
|
||||
Format nicht erkannt · Beispiele: <code>#ff6b9d</code> · <code>rgb(255, 107, 157)</code> · <code>hsl(340, 100%, 71%)</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const TOOL_DEFS = [
|
||||
{id:'cron', label:'⏱ Crontab', desc:'Crontab erklären und erstellen', ready:true,
|
||||
tabs:[{id:'explain',label:'Erklären'},{id:'build',label:'Erstellen'}],
|
||||
@@ -2370,7 +2689,7 @@ const TOOL_DEFS = [
|
||||
{id:'diff', label:'± Text Diff', desc:'Zwei Texte vergleichen', ready:true, component:TextDiff},
|
||||
{id:'netzwerk', label:'🌐 Netzwerk', desc:'IP, Subnetz, Geschwindigkeit, CIDR', ready:true, component:NetzwerkRechner},
|
||||
{id:'datetime', label:'📅 Datetime', desc:'Zeiten umrechnen · Geburtstag · Differenz', ready:true, component:DatetimeRechner},
|
||||
{id:'keygen', label:'🔐 Key-Gen', desc:'Sichere Keys & Secrets generieren', ready:true, component:KeyGen},
|
||||
{id:'farben', label:'🎨 Farben', desc:'HEX, RGB, HSL, OKLCH umrechnen', ready:true, component:FarbRechner},
|
||||
];
|
||||
|
||||
export default function DevTools({ toast, mobile }) {
|
||||
|
||||
Reference in New Issue
Block a user