mirror of
https://github.com/vibegif/vibegif.lol.git
synced 2026-04-07 02:12:12 +00:00
Delete assets/js/app.js
This commit is contained in:
337
assets/js/app.js
337
assets/js/app.js
@@ -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();
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user