diff --git a/assets/js/app.js b/assets/js/app.js deleted file mode 100644 index faf2afd..0000000 --- a/assets/js/app.js +++ /dev/null @@ -1,337 +0,0 @@ -(function () { - "use strict"; - - /* ── Config ── */ - const STORAGE_KEY = "vibegif_openrouter_api_key"; - - const MASTER_PROMPT = - "minimal black and white line doodle, single stroke, white background, kawaii style"; - - const MODELS = [ - { - id: "google/gemini-3.1-flash-image-preview", - label: "google/gemini-3.1-flash-image-preview", - supportsHalfK: true, - }, - { - id: "bytedance-seed/seedream-4.5", - label: "bytedance-seed/seedream-4.5", - supportsHalfK: false, - }, - ]; - - const ASPECT_RATIOS = [ - "1:1", "2:3", "3:2", "3:4", "4:3", - "4:5", "5:4", "9:16", "16:9", "21:9", - ]; - - const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"; - - const GIF_WORKER_URL = - "https://cdn.jsdelivr.net/npm/gif.js@0.2.0/dist/gif.worker.js"; - - /* ── Helpers ── */ - const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, Number(n) || lo)); - const modelById = (id) => MODELS.find((m) => m.id === id) || MODELS[0]; - - function refreshIcons() { - requestAnimationFrame(() => { - if (window.lucide) window.lucide.createIcons(); - }); - } - - /* ── OpenRouter ── */ - const SYSTEM_PROMPT = [ - "You generate a single image per request for an animation pipeline.", - "Keep style consistency across frames.", - "No text overlays, no labels, no typography.", - "Output only the image.", - ].join(" "); - - function buildPromptText(userPrompt, masterPrompt, frameCount, frameIndex) { - const lines = [ - "Create frame " + frameIndex + " of " + frameCount + " for an animated GIF.", - "Concept: " + userPrompt + ".", - "Master style lock: " + masterPrompt + ".", - "Use a white background and clean black line doodle style.", - "Keep the subject identity and scene composition coherent between frames.", - ]; - if (frameIndex === 1) { - lines.push("This is the first frame. Establish a clear starting pose."); - } else { - lines.push( - "Imagine we are trying to create a " + frameCount + - " frame gif. Generate the next meaningful frame." - ); - lines.push( - "This is frame " + frameIndex + - ". Advance the motion naturally from the prior frame references." - ); - } - lines.push("Return one meaningful image only."); - return lines.join("\n"); - } - - function buildUserContent(text, prevFrames) { - var parts = [{ type: "text", text: text }]; - var tail = prevFrames.slice(-2); - for (var i = 0; i < tail.length; i++) { - parts.push({ - type: "image_url", - image_url: { url: tail[i] }, - }); - } - return parts; - } - - function extractImageUrl(json) { - var msg = json && json.choices && json.choices[0] && json.choices[0].message; - if (!msg) return ""; - // OpenRouter image gen returns base64 URL inside content array - if (Array.isArray(msg.content)) { - for (var i = 0; i < msg.content.length; i++) { - var part = msg.content[i]; - if (part.type === "image_url") { - var url = part.image_url && (part.image_url.url || part.image_url); - if (url) return url; - } - } - } - // fallback: images array - if (Array.isArray(msg.images) && msg.images.length) { - var img = msg.images[0]; - return img.image_url?.url || img.imageUrl?.url || img.url || ""; - } - return ""; - } - - async function requestFrame(opts) { - var body = { - model: opts.model, - stream: false, - modalities: ["image"], - messages: [ - { role: "system", content: SYSTEM_PROMPT }, - { - role: "user", - content: buildUserContent( - buildPromptText(opts.userPrompt, opts.masterPrompt, opts.frameCount, opts.frameIndex), - opts.previousFrames - ), - }, - ], - image_config: { - image_size: opts.imageSize, - aspect_ratio: opts.aspectRatio, - }, - }; - - var headers = { - Authorization: "Bearer " + opts.apiKey, - "Content-Type": "application/json", - "HTTP-Referer": window.location.origin || "https://vibegif.lol", - "X-OpenRouter-Title": "vibegif.lol", - }; - - var res = await fetch(OPENROUTER_URL, { - method: "POST", - headers: headers, - body: JSON.stringify(body), - }); - - if (!res.ok) { - var raw = await res.text(); - var errMsg; - try { - var parsed = JSON.parse(raw); - errMsg = (parsed.error && parsed.error.message) || parsed.message || raw; - } catch (_) { - errMsg = raw || "Request failed (" + res.status + ")"; - } - throw new Error(errMsg); - } - - var json = await res.json(); - var url = extractImageUrl(json); - if (!url) throw new Error("No image returned. Try a simpler prompt or fewer frames."); - return url; - } - - /* ── GIF Builder ── */ - function loadImage(src) { - return new Promise(function (resolve, reject) { - var img = new Image(); - img.crossOrigin = "anonymous"; - img.onload = function () { resolve(img); }; - img.onerror = function () { reject(new Error("Failed to load frame image.")); }; - img.src = src; - }); - } - - async function createGif(frameUrls, fps) { - if (!frameUrls.length) throw new Error("No frames to encode."); - if (typeof window.GIF !== "function") throw new Error("GIF encoder not loaded."); - - var images = await Promise.all(frameUrls.map(loadImage)); - var first = images[0]; - var delay = Math.max(20, Math.round(1000 / clamp(fps, 1, 24))); - - return new Promise(function (resolve, reject) { - var gif = new window.GIF({ - workers: 2, - quality: 10, - repeat: 0, - width: first.naturalWidth || first.width, - height: first.naturalHeight || first.height, - workerScript: GIF_WORKER_URL, - }); - for (var i = 0; i < images.length; i++) { - gif.addFrame(images[i], { delay: delay }); - } - gif.on("finished", function (blob) { resolve(blob); }); - gif.on("abort", function () { reject(new Error("GIF rendering aborted.")); }); - try { gif.render(); } catch (e) { reject(e); } - }); - } - - /* ── Alpine Component ── */ - document.addEventListener("alpine:init", function () { - Alpine.data("vibeGifApp", function () { - return { - apiKey: "", - apiKeyInput: "", - showSettings: false, - model: MODELS[0].id, - imageSize: "1K", - aspectRatio: "1:1", - frameCount: 4, - fps: 4, - userPrompt: "rolling cat", - masterPrompt: MASTER_PROMPT, - frames: [], - gifUrl: "", - gifBlob: null, - isGenerating: false, - progressText: "", - errorText: "", - - init() { - var saved = localStorage.getItem(STORAGE_KEY) || ""; - this.apiKey = saved; - this.apiKeyInput = saved; - if (!saved) this.showSettings = true; - this.normalizeSelections(); - refreshIcons(); - }, - - get modelOptions() { return MODELS; }, - get selectedModel() { return modelById(this.model); }, - - get imageSizeOptions() { - return this.selectedModel.supportsHalfK ? ["1K", "0.5K"] : ["1K"]; - }, - - get aspectRatioOptions() { return ASPECT_RATIOS; }, - get hasApiKey() { return !!this.apiKey; }, - - get canGenerate() { - return this.hasApiKey && !this.isGenerating && this.userPrompt.trim().length > 0; - }, - - normalizeSelections() { - if (this.imageSizeOptions.indexOf(this.imageSize) === -1) { - this.imageSize = "1K"; - } - }, - - openSettings() { - this.apiKeyInput = this.apiKey; - this.showSettings = true; - this.$nextTick(refreshIcons); - }, - - closeSettings() { this.showSettings = false; }, - - saveApiKey() { - var key = (this.apiKeyInput || "").trim(); - this.apiKey = key; - if (key) localStorage.setItem(STORAGE_KEY, key); - else localStorage.removeItem(STORAGE_KEY); - this.showSettings = false; - }, - - clearApiKey() { - this.apiKey = ""; - this.apiKeyInput = ""; - localStorage.removeItem(STORAGE_KEY); - }, - - resetOutput() { - this.frames = []; - this.errorText = ""; - this.progressText = ""; - this.gifBlob = null; - if (this.gifUrl) { - URL.revokeObjectURL(this.gifUrl); - this.gifUrl = ""; - } - }, - - async generateGif() { - if (!this.canGenerate) { - if (!this.hasApiKey) this.openSettings(); - return; - } - - var fc = clamp(this.frameCount, 2, 24); - var fpsVal = clamp(this.fps, 1, 24); - var prompt = this.userPrompt.trim(); - this.frameCount = fc; - this.fps = fpsVal; - this.resetOutput(); - this.isGenerating = true; - - try { - for (var i = 1; i <= fc; i++) { - this.progressText = "Generating frame " + i + "/" + fc + "…"; - - var imgUrl = await requestFrame({ - apiKey: this.apiKey, - model: this.model, - userPrompt: prompt, - masterPrompt: this.masterPrompt, - frameCount: fc, - frameIndex: i, - previousFrames: this.frames.slice(-2), - imageSize: this.imageSize, - aspectRatio: this.aspectRatio, - }); - - this.frames.push(imgUrl); - } - - this.progressText = "Rendering GIF…"; - this.gifBlob = await createGif(this.frames, fpsVal); - this.gifUrl = URL.createObjectURL(this.gifBlob); - this.progressText = "Done ✨"; - this.$nextTick(refreshIcons); - } catch (err) { - this.errorText = err.message || "Generation failed."; - } finally { - this.isGenerating = false; - } - }, - - downloadGif() { - if (!this.gifUrl) return; - var a = document.createElement("a"); - a.href = this.gifUrl; - a.download = "vibegif-" + Date.now() + ".gif"; - document.body.appendChild(a); - a.click(); - a.remove(); - }, - }; - }); - }); -})();