Imagegen / index.html
Nuzwa's picture
Update index.html
9d86d4d verified
raw
history blame
22 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>imagegen</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap" rel="stylesheet">
<!-- Tracking removed per request (Smartlook + Google Tag). TODO: add GTM later if needed. -->
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; margin: 0; padding: 0; font-family: 'Poppins', sans-serif; -webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale; }
body { background: radial-gradient(circle at 20% 20%, rgba(140,60,200,0.08), transparent 40%), radial-gradient(circle at 80% 80%, rgba(200,60,255,0.05), transparent 40%), #000; color:#fff; overflow: hidden; }
.topbar {
position: fixed; top: 0; left: 0; right: 0; height: 56px;
display: flex; align-items: center; justify-content: space-between;
padding: 0 16px; gap: 12px; z-index: 1000; background: transparent;
}
.btn-small {
background: #141414; border-radius: 10px; padding: 8px 12px;
font-size: 13px; color: #fff; display:inline-flex; align-items:center; gap:10px;
border:1px solid rgba(255,255,255,0.03); cursor: pointer;
}
.btn-small img { width:14px; height:14px; display:block; }
.container { width: 70%; max-width: 1100px; margin: 5em auto 16px; padding: 0 12px; display: flex; flex-direction: column; gap: 12px; position: relative; }
@media (max-width: 768px) { .container { width: 100%; padding: 0 12px; margin-top:5em; } }
.input-row { display:flex; gap:10px; align-items:flex-start; }
textarea#prompt {
flex:1; min-width:0; max-height:250px; height:56px; resize:none; border-radius:12px; padding:12px;
background:#0e0e0e; border:1px solid rgba(255,255,255,0.03); color:#fff; font-size:14px; line-height:1.35; overflow:auto; outline:none;
}
.generate-btn {
background: linear-gradient(90deg,#ff93e9,#9b7cff); border:none; border-radius:12px; padding:12px 18px; font-size:14px; cursor:pointer; color:#fff;
min-height:56px; display:inline-flex; align-items:center; justify-content:center; transition:opacity .15s;
}
.generate-btn[disabled] { opacity:0.6; cursor:default; }
.options-row { display:flex; justify-content:space-between; align-items:center; }
.left-group, .right-group { display:flex; gap:8px; align-items:center; }
.modal-backdrop { display:none; position: fixed; inset: 0; background: rgba(0,0,0,0.72); align-items: center; justify-content: center; padding: 20px; z-index: 2000; }
.modal { width: 100%; max-width: 720px; background: #0f0f0f; border-radius: 12px; padding: 18px; box-shadow: 0 10px 40px rgba(0,0,0,0.6); color: #fff; }
@media (max-width:420px) { .modal { padding: 14px; max-width: calc(100% - 12px); border-radius: 10px; } }
.styles-grid { display:grid; grid-template-columns: repeat(2, 1fr); gap:10px; margin-top:12px; }
.style-card { background:#101010; border-radius:10px; overflow:hidden; cursor:pointer; text-align:center; border:2px solid transparent; padding-bottom:8px; font-size:13px; }
.style-card img { width:100%; height:72px; object-fit:cover; display:block; }
.style-card.selected { border-color:#ff93e9; box-shadow: 0 8px 30px rgba(155,124,255,0.06); }
.settings-row { display:flex; gap:12px; align-items:center; margin-top:10px; flex-wrap:wrap; }
.settings-row label { font-size:14px; display:flex; gap:8px; align-items:center; color:#eaeaea; }
.settings-row input[type="number"], .settings-row select { background:#0c0c0c; border:1px solid rgba(255,255,255,0.04); color:#fff; padding:6px 8px; border-radius:8px; font-size:14px; min-width:90px; }
.modal-actions { margin-top:14px; display:flex; justify-content:flex-end; gap:10px; }
.modal-actions button { background:#1a1a1a; border-radius:10px; padding:9px 12px; font-size:14px; color:#fff; border:1px solid rgba(255,255,255,0.03); cursor:pointer; }
#images { width: 100%; margin-top:8px; display:flex; gap:12px; flex-wrap:wrap; justify-content:center; align-items:flex-start; overflow:auto; padding:10px; border-radius:10px; background: linear-gradient(180deg, rgba(255,255,255,0.01), transparent); border:1px solid rgba(255,255,255,0.02); max-height: calc(100vh - 5em - 220px); }
.image-card { background:#0b0b0b; border-radius:12px; padding:8px; min-height:120px; width: min(350px, 90vw); box-shadow: 0 10px 30px rgba(0,0,0,0.6); border:1px solid rgba(255,255,255,0.02); display:flex; flex-direction:column; gap:8px; align-items:center; justify-content:center; }
.image-card img { width:100%; height:auto; display:block; border-radius:8px; }
.img-actions { display:flex; gap:8px; flex-wrap:wrap; justify-content:flex-end; width:100%; }
.img-actions button { background:#141414; border:1px solid rgba(255,255,255,0.08); color:#fff; border-radius:8px; padding:6px 10px; font-size:12px; cursor:pointer; }
.loader { border:3px solid #222; border-top:3px solid #ff93e9; border-radius:50%; width:22px; height:22px; animation:spin 1s linear infinite; }
@keyframes spin { 0%{ transform:rotate(0) } 100%{ transform:rotate(360deg) } }
.muted { color:#9b9bb9; font-size:13px; }
.note { font-size:13px; color:#cfcfcf; margin-top:6px; }
</style>
</head>
<body>
<div class="topbar" role="banner">
<!-- Replaced Discord invite with Manus invite -->
<div class="btn-small" onclick="window.open('https://manus.im/invitation/XHHHQTMBM0ZCTX')">Manus</div>
<!-- Keep Buy Me a Coffee visible but remove link for now -->
<!-- TODO: add Buy Me a Coffee link later -->
<a class="btn-small" aria-disabled="true">
<img src="https://cdn.buymeacoffee.com/buttons/bmc-new-btn-logo.svg" alt="coffee">
<span>Buy me a coffee</span>
</a>
</div>
<div class="container" role="main">
<div class="input-row" role="search">
<textarea id="prompt" placeholder="Enter your prompt..." aria-label="Prompt"></textarea>
<button id="generateBtn" class="generate-btn" aria-label="Generate">Generate</button>
</div>
<div class="options-row">
<div class="left-group">
<div id="styleBtn" class="btn-small" aria-haspopup="dialog">🎨 Style</div>
</div>
<div class="right-group">
<div id="settingsBtn" class="btn-small" aria-haspopup="dialog">⚙️ Settings</div>
</div>
</div>
<div class="note">Generated images (scroll this pane). New images appear at the top.</div>
<div id="images" aria-live="polite"></div>
</div>
<!-- Style & Framing Modal -->
<div id="styleModal" class="modal-backdrop" role="dialog" aria-modal="true" aria-hidden="true">
<div class="modal" role="document">
<h2 style="margin:0 0 6px 0;">Choose a style</h2>
<div class="note">Click a style — it will be applied silently to your request.</div>
<div class="styles-grid" id="stylesGrid">
<div class="style-card selected" data-name="none"><img src="none.jpg" alt="No style"><div>No style</div></div>
<div class="style-card" data-name="cinema"><img src="https://xyplon.web.app/assets/Cinematic.jpeg" alt="Cinema"><div>Cinema</div></div>
<div class="style-card" data-name="realistic"><img src="https://xyplon.web.app/assets/Realistic.jpeg" alt="Realistic"><div>Realistic</div></div>
<div class="style-card" data-name="photography"><img src="https://xyplon.web.app/assets/Photography.jpeg" alt="Photography"><div>Photography</div></div>
<div class="style-card" data-name="fantasy"><img src="https://xyplon.web.app/assets/Digital.jpeg" alt="Fantasy"><div>Fantasy</div></div>
</div>
<!-- Framing presets (one-line each) -->
<div class="settings-row" style="margin-top:14px;">
<label>Framing:
<select id="framing">
<option value="auto" selected>Auto</option>
<option value="close">Close‑up</option>
<option value="medium">Medium</option>
<option value="full">Full‑body</option>
</select>
</label>
</div>
<div class="modal-actions">
<button id="closeStyle">Close</button>
</div>
</div>
</div>
<!-- Settings Modal -->
<div id="settingsModal" class="modal-backdrop" role="dialog" aria-modal="true" aria-hidden="true">
<div class="modal" role="document">
<h2 style="margin:0 0 6px 0;">API Settings</h2>
<div class="settings-row">
<label>Model:
<select id="model">
<option value="flux">flux</option>
<option value="turbo">turbo (uncensored)</option>
</select>
</label>
<label>Aspect:
<select id="aspect">
<option value="custom" selected>Custom</option>
<option value="1:1">1:1</option>
<option value="16:9">16:9</option>
<option value="9:16">9:16</option>
<option value="3:2">3:2</option>
<option value="2:3">2:3</option>
<option value="4:5">4:5</option>
</select>
</label>
<label>Width:
<input id="width" type="number" min="64" max="2048" value="1024" />
</label>
<label>Height:
<input id="height" type="number" min="64" max="2048" value="1024" />
</label>
<label>Enhance:
<input id="enhance" type="checkbox" />
</label>
<label>Seed:
<input id="seed" type="number" value="42" />
</label>
<label style="display:flex;align-items:center;gap:6px;">
<input id="randomSeed" type="checkbox" checked /> Random
</label>
</div>
<div class="modal-actions">
<button id="closeSettings">Close</button>
<button id="saveSettings">Save</button>
</div>
</div>
</div>
<script>
const baseUrl = 'https://image.pollinations.ai';
let selectedStyle = null;
let isGenerating = false;
// --- Style templates (concise) ---
const styleTemplates = {
cinema: " cinematic lighting, letterbox aspect, rich color grading",
realistic: " realistic stock photo",
photography: "85mm lens, shallow depth of field, natural film grain",
fantasy: " epic fantasy, vibrant colors, surreal composition"
};
// --- Framing presets: one-line each ---
const framingTemplates = {
auto: "",
close: " tight framing, head-and-shoulders, 85mm lens, shallow depth of field",
medium: " waist-up, 50mm lens, natural perspective",
full: " head-to-toe, 35mm lens, subject fully in frame"
};
// --- Safer SYSTEM_PROMPT ---
const SYSTEM_PROMPT = `You are an assistant that enhances user prompts for high-quality, creative image generation with the Flux model.\n\nSafety: Do not produce or encourage illegal, harmful, violent, hateful, or sexual content (including minors), or content that violates platform policies. Keep outputs appropriate and respectful.\n\nGuidelines:\n- Prefer technical clarity (camera type, lens, exposure, lighting, environment) over vague adjectives.\n- Keep color natural; avoid oversaturation unless the user asks.\n- Choose a single coherent style; avoid mixing incompatible styles (e.g., do not mix "realistic" with "photography" keywords redundantly).\n- If the user already specified lens/shot details, do not duplicate them.\n- Keep the final prompt concise and descriptive; return only the enhanced prompt text with no preamble.`;
// --- Enhance prompt via text endpoint ---
async function enhancePrompt(userPrompt) {
try {
const messages = [
{ role: 'system', content: SYSTEM_PROMPT },
{ role: 'user', content: `"${userPrompt}"` }
];
const res = await fetch('https://text.pollinations.ai/openai', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'openai', messages })
});
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
return (data.choices?.[0]?.message?.content || userPrompt).trim();
} catch (e) {
console.error('Enhance fail:', e); return userPrompt;
}
}
// --- Persist settings ---
function saveSettingsToStorage() {
const s = {
model: document.getElementById('model').value,
aspect: document.getElementById('aspect').value,
width: Number(document.getElementById('width').value || 1024),
height: Number(document.getElementById('height').value || 1024),
seed: Number(document.getElementById('seed').value || 42),
randomSeed: document.getElementById('randomSeed').checked,
enhance: document.getElementById('enhance').checked
};
try { localStorage.setItem('ai_img_settings', JSON.stringify(s)); } catch(e){}
}
function loadSettingsFromStorage() {
try {
const s = JSON.parse(localStorage.getItem('ai_img_settings') || '{}');
if (s.model) document.getElementById('model').value = s.model;
if (s.aspect) document.getElementById('aspect').value = s.aspect;
if (s.width) document.getElementById('width').value = s.width;
if (s.height) document.getElementById('height').value = s.height;
if (s.seed || s.seed === 0) document.getElementById('seed').value = s.seed;
document.getElementById('randomSeed').checked = (s.randomSeed !== undefined) ? s.randomSeed : true;
document.getElementById('enhance').checked = (s.enhance !== undefined) ? s.enhance : true; // bug fixed
} catch(e){}
}
loadSettingsFromStorage();
// --- Aspect helpers ---
function parseAspect(aspectStr){ const [w,h] = aspectStr.split(':').map(Number); return (!w||!h)?1:(w/h); }
function sizeForAspect(aspectStr, base=1024){
const r = parseAspect(aspectStr);
if (r >= 1){ const width=base; const height=Math.max(64, Math.round(width/r)); return {width,height}; }
else { const height=base; const width=Math.max(64, Math.round(height*r)); return {width,height}; }
}
function applyAspectToFields(){
const aspect = document.getElementById('aspect').value;
if (aspect === 'custom') return; // respect manual values
const {width, height} = sizeForAspect(aspect, 1024);
document.getElementById('width').value = width;
document.getElementById('height').value = height;
}
// --- Modal plumbing ---
const styleModal = document.getElementById('styleModal');
const settingsModal = document.getElementById('settingsModal');
function openModal(modal) { modal.style.display = 'flex'; modal.setAttribute('aria-hidden','false'); document.body.style.overflow = 'hidden'; }
function closeModal(modal) { modal.style.display = 'none'; modal.setAttribute('aria-hidden','true'); document.body.style.overflow = ''; }
document.getElementById('styleBtn').addEventListener('click', () => openModal(styleModal));
document.getElementById('closeStyle').addEventListener('click', () => closeModal(styleModal));
document.getElementById('settingsBtn').addEventListener('click', () => openModal(settingsModal));
document.getElementById('closeSettings').addEventListener('click', () => closeModal(settingsModal));
document.getElementById('saveSettings').addEventListener('click', () => {
// clamp + optional aspect auto-size
const widthEl = document.getElementById('width');
const heightEl = document.getElementById('height');
widthEl.value = Math.max(64, Math.min(2048, Number(widthEl.value) || 1024));
heightEl.value = Math.max(64, Math.min(2048, Number(heightEl.value) || 1024));
applyAspectToFields();
saveSettingsToStorage();
closeModal(settingsModal);
});
document.querySelectorAll('.modal-backdrop').forEach(back => back.addEventListener('click', (e) => { if (e.target === back) closeModal(back); }));
window.addEventListener('keydown', (e) => { if (e.key === 'Escape') { if (styleModal.style.display === 'flex') closeModal(styleModal); if (settingsModal.style.display === 'flex') closeModal(settingsModal); } });
// Style select
document.querySelectorAll('.style-card').forEach(card => {
card.addEventListener('click', () => {
document.querySelectorAll('.style-card').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
selectedStyle = card.dataset.name === 'none' ? null : card.dataset.name;
});
});
// Aspect change auto-size
document.getElementById('aspect').addEventListener('change', applyAspectToFields);
// Prompt textarea auto-size
const promptEl = document.getElementById('prompt');
function autoResizeTextarea(){ promptEl.style.height='auto'; promptEl.style.height = Math.min(250, promptEl.scrollHeight) + 'px'; }
promptEl.addEventListener('input', autoResizeTextarea); setTimeout(autoResizeTextarea, 0);
// Seed UI
const randomSeedEl = document.getElementById('randomSeed');
const seedEl = document.getElementById('seed');
randomSeedEl.addEventListener('change', () => { seedEl.disabled = randomSeedEl.checked; });
seedEl.disabled = randomSeedEl.checked;
// Images + Generate
const imagesContainer = document.getElementById('images');
const generateBtn = document.getElementById('generateBtn');
function randomInt(min, max){ return Math.floor(Math.random()*(max-min+1))+min; }
function makeActionsBar(imgUrl, filename, finalPrompt, seed, width, height){
const bar = document.createElement('div');
bar.className = 'img-actions';
const dl = document.createElement('button');
dl.textContent = 'Download';
dl.addEventListener('click', () => { const a = document.createElement('a'); a.href = imgUrl; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); });
const openBtn = document.createElement('button');
openBtn.textContent = 'Open';
openBtn.addEventListener('click', () => window.open(imgUrl,'_blank'));
const cp = document.createElement('button');
cp.textContent = 'Copy prompt';
cp.addEventListener('click', async () => { try { await navigator.clipboard.writeText(finalPrompt); cp.textContent='Copied!'; setTimeout(()=>cp.textContent='Copy prompt',900);} catch(e){} });
const rerun = document.createElement('button');
rerun.textContent = 'Re‑generate';
rerun.addEventListener('click', () => {
document.getElementById('randomSeed').checked = false;
document.getElementById('seed').disabled = false;
document.getElementById('seed').value = seed;
document.getElementById('width').value = width;
document.getElementById('height').value = height;
document.getElementById('prompt').value = finalPrompt;
autoResizeTextarea();
saveSettingsToStorage();
generateImage();
});
bar.append(dl, openBtn, cp, rerun);
return bar;
}
async function generateImage(){
if (isGenerating) return;
const rawPrompt = promptEl.value.trim();
if (!rawPrompt){
const temp=document.createElement('div'); temp.className='image-card'; temp.innerHTML='<div class="muted">Please enter a prompt</div>'; imagesContainer.prepend(temp); setTimeout(()=>temp.remove(),1200); return;
}
isGenerating = true; generateBtn.disabled=true;
const card = document.createElement('div');
card.className='image-card';
card.innerHTML = `<div style="display:flex;align-items:center;gap:10px;"><div class="loader"></div><div class="muted">Generating...</div></div>`;
imagesContainer.prepend(card);
const enhanceOn = document.getElementById('enhance').checked;
const framingChoice = document.getElementById('framing').value;
// Build prompt (style + framing in concise one-liners)
let promptWithMods = rawPrompt;
if (selectedStyle && styleTemplates[selectedStyle]) promptWithMods += styleTemplates[selectedStyle];
if (framingTemplates[framingChoice]) promptWithMods += framingTemplates[framingChoice];
let finalPrompt = promptWithMods;
if (enhanceOn){
card.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;gap:10px;"><div class="loader"></div><div class="muted">Enhancing prompt...</div></div>`;
finalPrompt = await enhancePrompt(promptWithMods);
card.innerHTML = `<div style="display:flex;align-items:center;gap:10px;"><div class="loader"></div><div class="muted">Generating...</div></div>`;
}
// Params
const model = encodeURIComponent(document.getElementById('model').value || 'flux');
const width = Math.max(64, Math.min(2048, Number(document.getElementById('width').value) || 1024));
const height = Math.max(64, Math.min(2048, Number(document.getElementById('height').value) || 1024));
const seed = document.getElementById('randomSeed').checked ? randomInt(0, 4294967295) : (Number(document.getElementById('seed').value) || 42);
try{
const encodedPrompt = encodeURIComponent(finalPrompt);
const url = `${baseUrl}/prompt/${encodedPrompt}?model=${model}&width=${width}&height=${height}&seed=${seed}&nologo=true&safe=false`;
const res = await fetch(url);
if(!res.ok) throw new Error('Network '+res.status);
const blob = await res.blob();
const imgUrl = URL.createObjectURL(blob);
const filename = `image_${width}x${height}_seed${seed}.png`;
card.innerHTML = `<img src="${imgUrl}" alt="generated image">`;
card.appendChild(makeActionsBar(imgUrl, filename, finalPrompt, seed, width, height));
}catch(err){
console.error(err);
card.innerHTML = `<div class="muted" style="color:#ff6b6b;">Error generating image</div>`;
}finally{
isGenerating=false; generateBtn.disabled=false; saveSettingsToStorage();
}
}
document.getElementById('generateBtn').addEventListener('click', generateImage);
promptEl.addEventListener('keydown', (e)=>{ if(e.key==='Enter'&&(e.ctrlKey||e.metaKey)) generateImage(); });
window.addEventListener('beforeunload', saveSettingsToStorage);
</script>
</body>
</html>