import { generateImageFrame } from "./openrouter.js"; import { buildGifFromFrames } from "./gif.js"; import { firstPrompt, nextFramePrompt } from "./ui.js"; const GEMINI_MODEL = "google/gemini-3.1-flash-image-preview"; const state = { apiKey: localStorage.getItem("openrouter_api_key") || "", frames: [], gifUrl: "", loading: false }; const el = {}; const q = (id) => document.getElementById(id); function clamp(n, min, max, fallback) { n = Number(n); if (Number.isNaN(n)) return fallback; return Math.min(max, Math.max(min, n)); } function show(node) { node.classList.remove("hidden"); if (node.id === "settings-modal") node.classList.add("flex"); } function hide(node) { node.classList.add("hidden"); if (node.id === "settings-modal") node.classList.remove("flex"); } function setError(msg = "") { if (!msg) { el.error.textContent = ""; hide(el.error); return; } el.error.textContent = msg; show(el.error); } function setProgress(label = "", pct = 0, visible = false) { el.progressLabel.textContent = label; el.progressBar.style.width = `${Math.max(0, Math.min(100, pct))}%`; visible ? show(el.progressWrap) : hide(el.progressWrap); } function setLoading(v) { state.loading = !!v; el.generateBtn.disabled = state.loading; el.generateBtn.textContent = state.loading ? "Generating…" : "Generate GIF"; } function updateImageSizeOptions() { const model = el.model.value; const current = el.imageSize.value; const options = model === GEMINI_MODEL ? [ { value: "1K", label: "1K" }, { value: "0.5K", label: "0.5K" } ] : [{ value: "1K", label: "1K" }]; el.imageSize.innerHTML = ""; for (const opt of options) { const o = document.createElement("option"); o.value = opt.value; o.textContent = opt.label; el.imageSize.appendChild(o); } const allowed = options.map((o) => o.value); el.imageSize.value = allowed.includes(current) ? current : "1K"; } function renderFrames() { el.framesGrid.innerHTML = ""; for (let i = 0; i < state.frames.length; i++) { const card = document.createElement("div"); card.className = "rounded-xl border border-ui-border p-2 bg-white"; const img = document.createElement("img"); img.src = state.frames[i]; img.alt = `frame ${i + 1}`; img.className = "w-full rounded-lg"; const p = document.createElement("p"); p.className = "mt-1 text-xs text-ui-sub"; p.textContent = `Frame ${i + 1}`; card.appendChild(img); card.appendChild(p); el.framesGrid.appendChild(card); } } function renderGif() { if (!state.gifUrl) { hide(el.gifWrap); show(el.emptyOutput); return; } el.gifImg.src = state.gifUrl; el.gifDownload.href = state.gifUrl; show(el.gifWrap); hide(el.emptyOutput); } function openSettings() { el.apiKeyInput.value = state.apiKey; show(el.settingsModal); } function closeSettings() { hide(el.settingsModal); } function saveApiKey() { state.apiKey = el.apiKeyInput.value.trim(); localStorage.setItem("openrouter_api_key", state.apiKey); closeSettings(); } function clearApiKey() { state.apiKey = ""; el.apiKeyInput.value = ""; localStorage.removeItem("openrouter_api_key"); } async function generate() { setError(""); setLoading(true); setProgress("", 0, true); if (state.gifUrl) URL.revokeObjectURL(state.gifUrl); state.gifUrl = ""; state.frames = []; renderGif(); renderFrames(); const model = el.model.value; const userPrompt = el.userPrompt.value.trim(); const frameCount = clamp(el.frameCount.value, 2, 24, 4); const fps = clamp(el.fps.value, 1, 24, 6); const imageSize = el.imageSize.value; const aspectRatio = el.aspectRatio.value; if (!state.apiKey) { setError("Add your OpenRouter API key first (☰ button)."); openSettings(); setLoading(false); setProgress("", 0, false); return; } if (!userPrompt) { setError("Please enter a simple prompt (e.g. rolling cat)."); setLoading(false); setProgress("", 0, false); return; } try { const p1 = firstPrompt(userPrompt); setProgress(`Generating frame 1/${frameCount}...`, Math.round(100 / frameCount), true); const frame1 = await generateImageFrame({ apiKey: state.apiKey, model, textPrompt: p1, previousFrames: [], imageSize, aspectRatio }); state.frames.push(frame1); renderFrames(); for (let i = 2; i <= frameCount; i++) { setProgress(`Generating frame ${i}/${frameCount}...`, Math.round((i / frameCount) * 100), true); const next = await generateImageFrame({ apiKey: state.apiKey, model, textPrompt: nextFramePrompt(frameCount), previousFrames: state.frames.slice(-2), imageSize, aspectRatio }); state.frames.push(next); renderFrames(); } setProgress("Building GIF...", 100, true); state.gifUrl = await buildGifFromFrames(state.frames, fps); renderGif(); setProgress("Done.", 100, true); } catch (e) { setError(e?.message || "Failed to generate."); setProgress("", 0, false); } finally { setLoading(false); } } function bind() { el.openSettingsBtn.addEventListener("click", openSettings); el.closeSettingsBtn.addEventListener("click", closeSettings); el.saveApiKeyBtn.addEventListener("click", saveApiKey); el.clearApiKeyBtn.addEventListener("click", clearApiKey); el.generateBtn.addEventListener("click", generate); el.model.addEventListener("change", updateImageSizeOptions); el.settingsModal.addEventListener("click", (e) => { if (e.target === el.settingsModal) closeSettings(); }); document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeSettings(); }); } function init() { el.model = q("model"); el.userPrompt = q("user-prompt"); el.frameCount = q("frame-count"); el.fps = q("fps"); el.imageSize = q("image-size"); el.aspectRatio = q("aspect-ratio"); el.generateBtn = q("generate-btn"); el.error = q("error"); el.progressWrap = q("progress-wrap"); el.progressLabel = q("progress-label"); el.progressBar = q("progress-bar"); el.gifWrap = q("gif-wrap"); el.gifImg = q("gif-img"); el.gifDownload = q("gif-download"); el.emptyOutput = q("empty-output"); el.framesGrid = q("frames-grid"); el.settingsModal = q("settings-modal"); el.openSettingsBtn = q("open-settings-btn"); el.closeSettingsBtn = q("close-settings-btn"); el.apiKeyInput = q("api-key-input"); el.saveApiKeyBtn = q("save-api-key-btn"); el.clearApiKeyBtn = q("clear-api-key-btn"); updateImageSizeOptions(); renderGif(); bind(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); }