693 lines
27 KiB
JavaScript
693 lines
27 KiB
JavaScript
/**
|
||
* Logo AI Pixel-Perfect Optimizer
|
||
*
|
||
* Architektur:
|
||
* 1. Bild laden → anzeigen + segmentieren
|
||
* 2. Slider = reine Mathematik (keine KI)
|
||
* 3. KI-Feld = zwei Modi:
|
||
* a) SVG-Modus: schickt kompakten SVG-Code → KI gibt modifiziertes SVG zurück
|
||
* b) Bild-Modus: schickt Bild + Segment-Map → KI gibt JSON-Befehle zurück
|
||
* (translate, scale, stroke – KEINE Bild-Generierung)
|
||
*/
|
||
|
||
// ─── State ───────────────────────────────────────────────────
|
||
|
||
const state = {
|
||
originalImage: null,
|
||
originalDataUrl: null,
|
||
originalSvgCode: null,
|
||
currentSvgCode: null,
|
||
segments: [],
|
||
history: [],
|
||
mode: null, // 'image' | 'svg'
|
||
currentView: 'result',
|
||
// KI-Bild-Modus: Anpassungen als JSON-Befehle
|
||
segmentAdjustments: {}, // { segIndex: { dx, dy, scaleX, scaleY, thicknessBoost } }
|
||
};
|
||
|
||
// ─── DOM ─────────────────────────────────────────────────────
|
||
|
||
const previewContainer = document.getElementById('preview-container');
|
||
const logoInput = document.getElementById('logo-input');
|
||
const uploadZone = document.getElementById('upload-zone');
|
||
const aiPrompt = document.getElementById('ai-prompt');
|
||
const aiApiKey = document.getElementById('ai-apikey');
|
||
const aiProvider = document.getElementById('ai-provider');
|
||
const btnAiApply = document.getElementById('btn-ai-apply');
|
||
const aiLog = document.getElementById('ai-log');
|
||
|
||
// ─── Upload ──────────────────────────────────────────────────
|
||
|
||
uploadZone.addEventListener('click', () => logoInput.click());
|
||
uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.classList.add('drag-over'); });
|
||
uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('drag-over'));
|
||
uploadZone.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
uploadZone.classList.remove('drag-over');
|
||
if (e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]);
|
||
});
|
||
logoInput.addEventListener('change', (e) => { if (e.target.files[0]) handleFile(e.target.files[0]); });
|
||
|
||
function handleFile(file) {
|
||
const name = file.name.toLowerCase();
|
||
|
||
if (name.endsWith('.svg')) {
|
||
const reader = new FileReader();
|
||
reader.onload = (ev) => {
|
||
const svgCode = ev.target.result;
|
||
state.mode = 'svg';
|
||
state.originalSvgCode = svgCode;
|
||
state.currentSvgCode = svgCode;
|
||
state.history = [svgCode];
|
||
state.segmentAdjustments = {};
|
||
|
||
const optimized = optimizeSvg(svgCode);
|
||
const pathCount = (optimized.match(/<path/g) || []).length;
|
||
|
||
updateStatus('SVG (Vektor)', pathCount + ' Pfade', getSvgDimensions(svgCode));
|
||
renderResult();
|
||
logAi('info', `SVG geladen: ${pathCount} Pfade, ${(svgCode.length / 1024).toFixed(0)} KB`);
|
||
logAi('info', `Optimiert für KI: ${(optimized.length / 1024).toFixed(0)} KB`);
|
||
};
|
||
reader.readAsText(file);
|
||
} else {
|
||
const reader = new FileReader();
|
||
reader.onload = (ev) => {
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
state.originalImage = img;
|
||
state.originalDataUrl = ev.target.result;
|
||
state.mode = 'image';
|
||
state.segments = detectSegments(img);
|
||
state.segmentAdjustments = {};
|
||
state.history = [JSON.stringify({})];
|
||
|
||
updateStatus('Bild (Pixel-Segmente)', state.segments.length + ' Segmente', `${img.width}×${img.height}`);
|
||
renderResult();
|
||
logAi('info', `Bild geladen: ${img.width}×${img.height}`);
|
||
logAi('info', `${state.segments.length} Buchstaben/Segmente erkannt – bereit für KI-Steuerung.`);
|
||
};
|
||
img.src = ev.target.result;
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
}
|
||
|
||
// ─── Segment-Erkennung ──────────────────────────────────────
|
||
|
||
function detectSegments(img) {
|
||
const tmpCanvas = document.createElement('canvas');
|
||
tmpCanvas.width = img.width;
|
||
tmpCanvas.height = img.height;
|
||
const ctx = tmpCanvas.getContext('2d');
|
||
ctx.drawImage(img, 0, 0);
|
||
|
||
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
||
const px = imageData.data;
|
||
const w = img.width, h = img.height;
|
||
|
||
const colActive = new Uint8Array(w);
|
||
for (let x = 0; x < w; x++) {
|
||
for (let y = 0; y < h; y++) {
|
||
const i = (y * w + x) * 4;
|
||
if (px[i + 3] > 20 && (px[i] < 240 || px[i + 1] < 240 || px[i + 2] < 240)) {
|
||
colActive[x] = 1;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
const segs = [];
|
||
let inside = false, start = 0;
|
||
for (let x = 0; x <= w; x++) {
|
||
const a = x < w ? colActive[x] : 0;
|
||
if (a && !inside) { start = x; inside = true; }
|
||
else if (!a && inside) {
|
||
segs.push({ x: start, width: x - start, index: segs.length });
|
||
inside = false;
|
||
}
|
||
}
|
||
if (segs.length === 0) segs.push({ x: 0, width: w, index: 0 });
|
||
return segs;
|
||
}
|
||
|
||
// ─── SVG-Optimierung (für KI-Token-Limit) ───────────────────
|
||
|
||
function optimizeSvg(svgCode) {
|
||
let s = svgCode;
|
||
// Koordinaten-Präzision auf 1 Dezimalstelle reduzieren
|
||
s = s.replace(/(\d+\.\d{2,})/g, (m) => parseFloat(m).toFixed(1));
|
||
// Überflüssige Whitespace entfernen
|
||
s = s.replace(/\s+/g, ' ');
|
||
s = s.replace(/>\s+</g, '><');
|
||
// Leere Pfade entfernen
|
||
s = s.replace(/<path[^>]*d="[^"]{0,5}"[^>]*\/>/g, '');
|
||
return s.trim();
|
||
}
|
||
|
||
// ─── Rendering ───────────────────────────────────────────────
|
||
|
||
const sliderIds = ['spacing', 'thickness', 'scale', 'flatten'];
|
||
sliderIds.forEach(id => {
|
||
document.getElementById(id).addEventListener('input', () => {
|
||
updateSliderDisplay();
|
||
renderResult();
|
||
});
|
||
});
|
||
|
||
function updateSliderDisplay() {
|
||
document.getElementById('val-spacing').textContent = document.getElementById('spacing').value + ' px';
|
||
document.getElementById('val-thickness').textContent = document.getElementById('thickness').value + ' px';
|
||
document.getElementById('val-scale').textContent = document.getElementById('scale').value + '%';
|
||
document.getElementById('val-flatten').textContent = document.getElementById('flatten').value + '%';
|
||
}
|
||
updateSliderDisplay();
|
||
|
||
function renderResult() {
|
||
if (state.currentView === 'original') { renderOriginal(); return; }
|
||
if (state.currentView === 'diff') { renderDiff(); return; }
|
||
if (!state.originalImage && !state.originalSvgCode) return;
|
||
|
||
const spacing = parseFloat(document.getElementById('spacing').value);
|
||
const thickness = parseFloat(document.getElementById('thickness').value);
|
||
const scalePct = parseFloat(document.getElementById('scale').value) / 100;
|
||
const flattenPct = parseFloat(document.getElementById('flatten').value) / 100;
|
||
|
||
if (state.mode === 'image' && state.originalImage) {
|
||
renderImageSegments(spacing, thickness, scalePct, flattenPct);
|
||
} else if (state.currentSvgCode) {
|
||
renderSvgWithParams(spacing, thickness, scalePct, flattenPct);
|
||
}
|
||
}
|
||
|
||
function renderImageSegments(spacing, thickness, scalePct, flattenPct) {
|
||
const img = state.originalImage;
|
||
const segments = state.segments;
|
||
const adj = state.segmentAdjustments;
|
||
|
||
let totalW = 0;
|
||
for (let i = 0; i < segments.length; i++) {
|
||
const seg = segments[i];
|
||
const a = adj[i] || {};
|
||
const sx = (a.scaleX || 1) * scalePct;
|
||
totalW += seg.width * sx + spacing + (a.dx || 0);
|
||
}
|
||
totalW = Math.max(totalW - spacing, img.width * scalePct);
|
||
|
||
const scaledH = img.height * scalePct * flattenPct;
|
||
const pad = Math.max(thickness * 2, 4) + 10;
|
||
const cw = Math.ceil(totalW + pad * 2);
|
||
const ch = Math.ceil(scaledH + pad * 2 + 40);
|
||
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = cw;
|
||
canvas.height = ch;
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.clearRect(0, 0, cw, ch);
|
||
|
||
let cx = pad;
|
||
for (let i = 0; i < segments.length; i++) {
|
||
const seg = segments[i];
|
||
const a = adj[i] || {};
|
||
const sx = (a.scaleX || 1) * scalePct;
|
||
const sy = (a.scaleY || 1) * scalePct * flattenPct;
|
||
const tb = (a.thicknessBoost || 0) + thickness;
|
||
const dx = a.dx || 0;
|
||
const dy = a.dy || 0;
|
||
|
||
const sw = seg.width * sx;
|
||
const sh = img.height * sy;
|
||
|
||
if (tb > 0) {
|
||
ctx.filter = `blur(${tb / 2}px) brightness(1.15)`;
|
||
ctx.drawImage(img, seg.x, 0, seg.width, img.height, cx + dx, pad + dy, sw, sh);
|
||
}
|
||
ctx.filter = 'none';
|
||
ctx.drawImage(img, seg.x, 0, seg.width, img.height, cx + dx, pad + dy, sw, sh);
|
||
cx += sw + spacing + dx;
|
||
}
|
||
|
||
previewContainer.innerHTML = '';
|
||
canvas.style.maxWidth = '100%';
|
||
canvas.style.height = 'auto';
|
||
previewContainer.appendChild(canvas);
|
||
}
|
||
|
||
function renderSvgWithParams(spacing, thickness, scalePct, flattenPct) {
|
||
const parser = new DOMParser();
|
||
const doc = parser.parseFromString(state.currentSvgCode, 'image/svg+xml');
|
||
const svg = doc.querySelector('svg');
|
||
if (!svg) return;
|
||
|
||
let rootG = svg.querySelector('g#logo-root');
|
||
if (!rootG) {
|
||
rootG = doc.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||
rootG.setAttribute('id', 'logo-root');
|
||
while (svg.childNodes.length > 0) rootG.appendChild(svg.childNodes[0]);
|
||
svg.appendChild(rootG);
|
||
}
|
||
rootG.setAttribute('transform', `scale(${scalePct}, ${scalePct * flattenPct})`);
|
||
|
||
if (thickness > 0) {
|
||
svg.querySelectorAll('path').forEach(p => {
|
||
const fill = p.getAttribute('fill') || '#000';
|
||
p.setAttribute('stroke', fill);
|
||
p.setAttribute('stroke-width', thickness.toString());
|
||
p.setAttribute('stroke-linejoin', 'round');
|
||
});
|
||
}
|
||
|
||
const vb = svg.getAttribute('viewBox');
|
||
if (vb && spacing !== 0) {
|
||
const parts = vb.split(/[\s,]+/).map(Number);
|
||
parts[2] += spacing * 2;
|
||
svg.setAttribute('viewBox', parts.join(' '));
|
||
}
|
||
|
||
previewContainer.innerHTML = new XMLSerializer().serializeToString(svg);
|
||
}
|
||
|
||
function renderOriginal() {
|
||
previewContainer.innerHTML = '';
|
||
if (state.originalDataUrl) {
|
||
const img = document.createElement('img');
|
||
img.src = state.originalDataUrl;
|
||
previewContainer.appendChild(img);
|
||
} else if (state.originalSvgCode) {
|
||
previewContainer.innerHTML = state.originalSvgCode;
|
||
}
|
||
}
|
||
|
||
function renderDiff() {
|
||
previewContainer.innerHTML = '';
|
||
const wrapper = document.createElement('div');
|
||
wrapper.style.cssText = 'display:flex;gap:20px;align-items:center;';
|
||
|
||
const leftDiv = document.createElement('div');
|
||
leftDiv.style.cssText = 'text-align:center;flex:1;';
|
||
const leftLabel = document.createElement('div');
|
||
leftLabel.textContent = 'ORIGINAL';
|
||
leftLabel.style.cssText = 'font-size:11px;color:#999;margin-bottom:8px;letter-spacing:1px;';
|
||
leftDiv.appendChild(leftLabel);
|
||
|
||
if (state.originalDataUrl) {
|
||
const im = document.createElement('img');
|
||
im.src = state.originalDataUrl;
|
||
im.style.maxWidth = '100%';
|
||
leftDiv.appendChild(im);
|
||
} else if (state.originalSvgCode) {
|
||
const d = document.createElement('div');
|
||
d.innerHTML = state.originalSvgCode;
|
||
leftDiv.appendChild(d);
|
||
}
|
||
|
||
const arrow = document.createElement('div');
|
||
arrow.textContent = '\u2192';
|
||
arrow.style.cssText = 'font-size:2em;color:#666;';
|
||
|
||
const rightDiv = document.createElement('div');
|
||
rightDiv.style.cssText = 'text-align:center;flex:1;';
|
||
const rightLabel = document.createElement('div');
|
||
rightLabel.textContent = 'OPTIMIERT';
|
||
rightLabel.style.cssText = 'font-size:11px;color:#4a9eff;margin-bottom:8px;letter-spacing:1px;';
|
||
rightDiv.appendChild(rightLabel);
|
||
|
||
const tmpContainer = document.createElement('div');
|
||
state.currentView = 'result';
|
||
renderResult();
|
||
tmpContainer.innerHTML = previewContainer.innerHTML;
|
||
state.currentView = 'diff';
|
||
rightDiv.appendChild(tmpContainer);
|
||
|
||
wrapper.appendChild(leftDiv);
|
||
wrapper.appendChild(arrow);
|
||
wrapper.appendChild(rightDiv);
|
||
previewContainer.innerHTML = '';
|
||
previewContainer.appendChild(wrapper);
|
||
}
|
||
|
||
// ─── View Toggle ─────────────────────────────────────────────
|
||
|
||
document.querySelectorAll('.view-toggle button').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.view-toggle button').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
state.currentView = btn.dataset.view;
|
||
renderResult();
|
||
});
|
||
});
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// ─── KI-Integration ──────────────────────────────────────────
|
||
// ═══════════════════════════════════════════════════════════════
|
||
|
||
// Zwei verschiedene System-Prompts: einer für SVG, einer für Bild-Segment-Modus
|
||
|
||
const SVG_SYSTEM_PROMPT = `Du bist ein SVG-Pfad-Manipulations-Experte.
|
||
|
||
STRIKTE REGELN – Verstoß = Ergebnis wird verworfen:
|
||
|
||
1. Du erhältst SVG-Code und eine Anweisung.
|
||
2. Du darfst NUR mathematische Transformationen anwenden:
|
||
translate(), scale(), stroke-width, transform, fill, opacity.
|
||
3. Alle Pfad-Daten (d="...") bleiben EXAKT erhalten,
|
||
außer der Benutzer sagt explizit "Pfad ändern/korrigieren".
|
||
4. KEINE neuen Pfade oder Formen erfinden.
|
||
5. Antworte NUR mit dem modifizierten SVG-Code.
|
||
Kein Markdown, keine Erklärungen, kein \`\`\`-Wrapper.
|
||
Nur <svg>...</svg>.
|
||
6. Wenn unmöglich, antworte: FEHLER: [Begründung]`;
|
||
|
||
const IMAGE_SYSTEM_PROMPT = `Du bist ein Logo-Analyse-Experte. Du erhältst ein Logo-Bild und eine Liste erkannter Segmente (Buchstaben/Formen).
|
||
|
||
Deine Aufgabe: Analysiere das Bild, verstehe welches Segment welcher Buchstabe/Form ist,
|
||
und gib NUR mathematische Transformations-Befehle als JSON zurück.
|
||
|
||
Erlaubte Befehle pro Segment:
|
||
- dx: Horizontale Verschiebung in Pixeln (positiv = rechts)
|
||
- dy: Vertikale Verschiebung in Pixeln (positiv = unten)
|
||
- scaleX: Horizontale Skalierung (1.0 = unverändert)
|
||
- scaleY: Vertikale Skalierung (1.0 = unverändert)
|
||
- thicknessBoost: Zusätzliche Strichdicke in Pixeln
|
||
|
||
STRIKTE REGELN:
|
||
1. Antworte NUR mit einem JSON-Objekt. Kein Text, kein Markdown.
|
||
2. Keys sind Segment-Indizes (als String), Values sind Objekte mit den obigen Feldern.
|
||
3. Nur Segmente auflisten, die sich ÄNDERN. Unveränderte weglassen.
|
||
4. KEINE Farben ändern, KEINE Formen neuzeichnen, KEINE Pixel manipulieren.
|
||
5. Du verschiebst/skalierst nur existierende Bildausschnitte.
|
||
|
||
Beispiel-Antwort:
|
||
{"0": {"dx": 15}, "2": {"scaleX": 1.1, "thicknessBoost": 0.5}, "4": {"dx": -5, "dy": 2}}`;
|
||
|
||
aiApiKey.addEventListener('input', () => {
|
||
btnAiApply.disabled = !aiApiKey.value.trim();
|
||
});
|
||
|
||
btnAiApply.addEventListener('click', () => {
|
||
const prompt = aiPrompt.value.trim();
|
||
const apiKey = aiApiKey.value.trim();
|
||
if (!prompt || !apiKey) return;
|
||
applyAiCorrection(prompt, apiKey);
|
||
});
|
||
|
||
async function applyAiCorrection(userPrompt, apiKey) {
|
||
btnAiApply.disabled = true;
|
||
btnAiApply.innerHTML = '<span class="spinner"></span> KI analysiert...';
|
||
logAi('info', `Sende an KI: "${userPrompt}"`);
|
||
|
||
const provider = aiProvider.value;
|
||
|
||
try {
|
||
if (state.mode === 'svg' && state.currentSvgCode) {
|
||
await handleSvgAi(userPrompt, apiKey, provider);
|
||
} else if (state.mode === 'image' && state.originalImage) {
|
||
await handleImageAi(userPrompt, apiKey, provider);
|
||
} else {
|
||
logAi('error', 'Bitte zuerst ein Logo laden.');
|
||
}
|
||
} catch (err) {
|
||
logAi('error', 'API-Fehler: ' + err.message);
|
||
} finally {
|
||
btnAiApply.disabled = false;
|
||
btnAiApply.textContent = 'KI-Korrektur anwenden';
|
||
}
|
||
}
|
||
|
||
// ─── SVG-Modus: kompakten SVG-Code an KI ────────────────────
|
||
|
||
async function handleSvgAi(userPrompt, apiKey, provider) {
|
||
const optimized = optimizeSvg(state.currentSvgCode);
|
||
const sizeKB = (optimized.length / 1024).toFixed(0);
|
||
logAi('info', `SVG optimiert: ${sizeKB} KB`);
|
||
|
||
if (optimized.length > 80000) {
|
||
logAi('error', `SVG immer noch zu gross (${sizeKB} KB). Versuche, ein einfacheres SVG zu laden oder das Logo als PNG zu importieren.`);
|
||
return;
|
||
}
|
||
|
||
const messages = [
|
||
{ role: 'user', content: `SVG-Code:\n\n${optimized}\n\nAnweisung: ${userPrompt}` }
|
||
];
|
||
|
||
let result;
|
||
if (provider === 'openai') {
|
||
result = await callOpenAI(messages, SVG_SYSTEM_PROMPT, apiKey);
|
||
} else {
|
||
result = await callAnthropic(messages, SVG_SYSTEM_PROMPT, apiKey);
|
||
}
|
||
|
||
if (result.startsWith('FEHLER:')) { logAi('error', result); return; }
|
||
if (!result.includes('<svg') || !result.includes('</svg>')) {
|
||
logAi('error', 'KI hat kein gültiges SVG zurückgegeben. Verworfen.');
|
||
return;
|
||
}
|
||
|
||
const origPaths = (state.currentSvgCode.match(/<path/g) || []).length;
|
||
const newPaths = (result.match(/<path/g) || []).length;
|
||
if (Math.abs(origPaths - newPaths) > 2) {
|
||
logAi('error', `Halluzinations-Schutz: ${origPaths} → ${newPaths} Pfade. Verworfen.`);
|
||
return;
|
||
}
|
||
|
||
state.history.push(state.currentSvgCode);
|
||
state.currentSvgCode = result;
|
||
renderResult();
|
||
logAi('info', `Korrektur angewendet. Pfade: ${origPaths} → ${newPaths}.`);
|
||
}
|
||
|
||
// ─── Bild-Modus: Bild + Segment-Map an KI ──────────────────
|
||
|
||
async function handleImageAi(userPrompt, apiKey, provider) {
|
||
const segments = state.segments;
|
||
|
||
// Kleine Version des Bildes erstellen (max 512px breit) für die KI
|
||
const maxW = 512;
|
||
const img = state.originalImage;
|
||
const ratio = Math.min(maxW / img.width, 1);
|
||
const thumbCanvas = document.createElement('canvas');
|
||
thumbCanvas.width = Math.round(img.width * ratio);
|
||
thumbCanvas.height = Math.round(img.height * ratio);
|
||
const tCtx = thumbCanvas.getContext('2d');
|
||
tCtx.drawImage(img, 0, 0, thumbCanvas.width, thumbCanvas.height);
|
||
const thumbDataUrl = thumbCanvas.toDataURL('image/png', 0.8);
|
||
|
||
const segmentDesc = segments.map((s, i) =>
|
||
`Segment ${i}: x=${s.x}, breite=${s.width}px (${((s.x / img.width) * 100).toFixed(0)}% von links)`
|
||
).join('\n');
|
||
|
||
logAi('info', `Bild-Modus: ${segments.length} Segmente, Thumbnail ${thumbCanvas.width}×${thumbCanvas.height}`);
|
||
|
||
let result;
|
||
if (provider === 'openai') {
|
||
const messages = [{
|
||
role: 'user',
|
||
content: [
|
||
{ type: 'image_url', image_url: { url: thumbDataUrl, detail: 'low' } },
|
||
{ type: 'text', text: `Segmente:\n${segmentDesc}\n\nAnweisung: ${userPrompt}` }
|
||
]
|
||
}];
|
||
result = await callOpenAI(messages, IMAGE_SYSTEM_PROMPT, apiKey);
|
||
} else {
|
||
const base64 = thumbDataUrl.split(',')[1];
|
||
const messages = [{
|
||
role: 'user',
|
||
content: [
|
||
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: base64 } },
|
||
{ type: 'text', text: `Segmente:\n${segmentDesc}\n\nAnweisung: ${userPrompt}` }
|
||
]
|
||
}];
|
||
result = await callAnthropic(messages, IMAGE_SYSTEM_PROMPT, apiKey);
|
||
}
|
||
|
||
// JSON parsen
|
||
let adjustments;
|
||
try {
|
||
const jsonMatch = result.match(/\{[\s\S]*\}/);
|
||
if (!jsonMatch) throw new Error('Kein JSON in Antwort');
|
||
adjustments = JSON.parse(jsonMatch[0]);
|
||
} catch (e) {
|
||
logAi('error', `KI-Antwort konnte nicht gelesen werden: ${e.message}`);
|
||
logAi('error', `Rohe Antwort: ${result.substring(0, 200)}`);
|
||
return;
|
||
}
|
||
|
||
// Validierung: nur erlaubte Felder, keine verrückten Werte
|
||
const allowedFields = ['dx', 'dy', 'scaleX', 'scaleY', 'thicknessBoost'];
|
||
for (const [key, val] of Object.entries(adjustments)) {
|
||
const idx = parseInt(key);
|
||
if (isNaN(idx) || idx < 0 || idx >= segments.length) {
|
||
logAi('error', `Ungültiger Segment-Index: ${key}. Verworfen.`);
|
||
return;
|
||
}
|
||
for (const field of Object.keys(val)) {
|
||
if (!allowedFields.includes(field)) {
|
||
logAi('error', `Verbotenes Feld "${field}" in Segment ${key}. Verworfen.`);
|
||
return;
|
||
}
|
||
}
|
||
if (val.scaleX && (val.scaleX < 0.3 || val.scaleX > 3)) {
|
||
logAi('error', `scaleX=${val.scaleX} in Segment ${key} zu extrem. Verworfen.`);
|
||
return;
|
||
}
|
||
if (val.scaleY && (val.scaleY < 0.3 || val.scaleY > 3)) {
|
||
logAi('error', `scaleY=${val.scaleY} in Segment ${key} zu extrem. Verworfen.`);
|
||
return;
|
||
}
|
||
if (val.dx && Math.abs(val.dx) > 200) {
|
||
logAi('error', `dx=${val.dx} in Segment ${key} zu extrem. Verworfen.`);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Undo-Stack
|
||
state.history.push(JSON.stringify(state.segmentAdjustments));
|
||
state.segmentAdjustments = { ...state.segmentAdjustments };
|
||
for (const [key, val] of Object.entries(adjustments)) {
|
||
state.segmentAdjustments[key] = {
|
||
...(state.segmentAdjustments[key] || {}),
|
||
...val
|
||
};
|
||
}
|
||
|
||
renderResult();
|
||
|
||
const changedCount = Object.keys(adjustments).length;
|
||
logAi('info', `${changedCount} Segment(e) angepasst: ${JSON.stringify(adjustments)}`);
|
||
}
|
||
|
||
// ─── API-Calls ───────────────────────────────────────────────
|
||
|
||
async function callOpenAI(messages, systemPrompt, apiKey) {
|
||
const res = await fetch('https://api.openai.com/v1/chat/completions', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
|
||
body: JSON.stringify({
|
||
model: 'gpt-4o',
|
||
temperature: 0,
|
||
max_tokens: 8000,
|
||
messages: [{ role: 'system', content: systemPrompt }, ...messages],
|
||
}),
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const err = await res.json().catch(() => ({}));
|
||
throw new Error(err.error?.message || `HTTP ${res.status}`);
|
||
}
|
||
|
||
const data = await res.json();
|
||
let content = data.choices[0].message.content.trim();
|
||
content = content.replace(/^```(?:xml|svg|html|json)?\n?/i, '').replace(/\n?```$/i, '');
|
||
return content;
|
||
}
|
||
|
||
async function callAnthropic(messages, systemPrompt, apiKey) {
|
||
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'x-api-key': apiKey,
|
||
'anthropic-version': '2023-06-01',
|
||
'anthropic-dangerous-direct-browser-access': 'true',
|
||
},
|
||
body: JSON.stringify({
|
||
model: 'claude-sonnet-4-20250514',
|
||
max_tokens: 8000,
|
||
temperature: 0,
|
||
system: systemPrompt,
|
||
messages: messages,
|
||
}),
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const err = await res.json().catch(() => ({}));
|
||
throw new Error(err.error?.message || `HTTP ${res.status}`);
|
||
}
|
||
|
||
const data = await res.json();
|
||
let content = data.content[0].text.trim();
|
||
content = content.replace(/^```(?:xml|svg|html|json)?\n?/i, '').replace(/\n?```$/i, '');
|
||
return content;
|
||
}
|
||
|
||
// ─── Log ─────────────────────────────────────────────────────
|
||
|
||
function logAi(type, msg) {
|
||
aiLog.classList.add('visible');
|
||
const line = document.createElement('div');
|
||
line.className = type;
|
||
const time = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||
line.textContent = `[${time}] ${msg}`;
|
||
aiLog.appendChild(line);
|
||
aiLog.scrollTop = aiLog.scrollHeight;
|
||
}
|
||
|
||
// ─── Undo ────────────────────────────────────────────────────
|
||
|
||
document.getElementById('btn-undo').addEventListener('click', () => {
|
||
if (state.history.length <= 1) return;
|
||
|
||
state.history.pop();
|
||
const prev = state.history[state.history.length - 1];
|
||
|
||
if (state.mode === 'svg') {
|
||
state.currentSvgCode = prev;
|
||
} else {
|
||
try { state.segmentAdjustments = JSON.parse(prev); }
|
||
catch { state.segmentAdjustments = {}; }
|
||
}
|
||
renderResult();
|
||
logAi('info', 'Rückgängig gemacht.');
|
||
});
|
||
|
||
// ─── Download ────────────────────────────────────────────────
|
||
|
||
document.getElementById('btn-download').addEventListener('click', () => {
|
||
const svg = previewContainer.querySelector('svg');
|
||
const canvas = previewContainer.querySelector('canvas');
|
||
|
||
if (svg) {
|
||
const svgData = new XMLSerializer().serializeToString(svg);
|
||
downloadFile('optimiertes-logo.svg', 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgData));
|
||
|
||
const tmpImg = new Image();
|
||
const blob = new Blob([svgData], { type: 'image/svg+xml' });
|
||
const url = URL.createObjectURL(blob);
|
||
tmpImg.onload = () => {
|
||
const c = document.createElement('canvas');
|
||
c.width = tmpImg.width * 2;
|
||
c.height = tmpImg.height * 2;
|
||
const ct = c.getContext('2d');
|
||
ct.drawImage(tmpImg, 0, 0, c.width, c.height);
|
||
downloadFile('optimiertes-logo.png', c.toDataURL('image/png'));
|
||
URL.revokeObjectURL(url);
|
||
};
|
||
tmpImg.src = url;
|
||
} else if (canvas) {
|
||
downloadFile('optimiertes-logo.png', canvas.toDataURL('image/png'));
|
||
}
|
||
});
|
||
|
||
function downloadFile(name, href) {
|
||
const a = document.createElement('a');
|
||
a.download = name;
|
||
a.href = href;
|
||
a.click();
|
||
}
|
||
|
||
document.getElementById('btn-fit').addEventListener('click', () => renderResult());
|
||
|
||
// ─── Status ──────────────────────────────────────────────────
|
||
|
||
function updateStatus(mode, segments, size) {
|
||
document.getElementById('status-mode').textContent = mode;
|
||
document.getElementById('status-segments').textContent = segments;
|
||
document.getElementById('status-size').textContent = size;
|
||
}
|
||
|
||
function getSvgDimensions(svgCode) {
|
||
const m = svgCode.match(/viewBox="([^"]+)"/);
|
||
if (m) { const p = m[1].split(/[\s,]+/); return `${p[2]}×${p[3]}`; }
|
||
return '\u2013';
|
||
}
|