Files
vibegif.lol/assets/js/app.js

199 lines
4.7 KiB
JavaScript

import {
STORAGE_KEYS,
MASTER_PROMPT,
MODELS,
BASE_ASPECT_RATIOS,
GEMINI_EXTRA_ASPECT_RATIOS,
} from "./config.js";
import { requestFrameImage } from "./openrouter.js";
import { createGifFromFrames } from "./gif-builder.js";
const clamp = (n, min, max) => Math.max(min, Math.min(max, Number(n) || min));
const modelById = (id) => MODELS.find((m) => m.id === id) || MODELS[0];
function vibeGifApp() {
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() {
const saved = localStorage.getItem(STORAGE_KEYS.apiKey) || "";
this.apiKey = saved;
this.apiKeyInput = saved;
this.showSettings = !saved;
this.normalizeSelections();
this.$watch("model", () => this.normalizeSelections());
this.refreshIcons();
},
get modelOptions() {
return MODELS;
},
get selectedModel() {
return modelById(this.model);
},
get imageSizeOptions() {
const opts = ["1K"];
if (this.selectedModel.supportsHalfK) opts.push("0.5K");
return opts;
},
get aspectRatioOptions() {
const opts = [...BASE_ASPECT_RATIOS];
if (this.selectedModel.supportsExtendedAspectRatios) {
opts.push(...GEMINI_EXTRA_ASPECT_RATIOS);
}
return opts;
},
get hasApiKey() {
return !!this.apiKey;
},
get canGenerate() {
return this.hasApiKey && !this.isGenerating && this.userPrompt.trim().length > 0;
},
normalizeSelections() {
if (!this.imageSizeOptions.includes(this.imageSize)) {
this.imageSize = this.imageSizeOptions[0];
}
if (!this.aspectRatioOptions.includes(this.aspectRatio)) {
this.aspectRatio = "1:1";
}
},
openSettings() {
this.apiKeyInput = this.apiKey;
this.showSettings = true;
this.refreshIcons();
},
closeSettings() {
this.showSettings = false;
},
saveApiKey() {
const key = (this.apiKeyInput || "").trim();
this.apiKey = key;
if (key) localStorage.setItem(STORAGE_KEYS.apiKey, key);
else localStorage.removeItem(STORAGE_KEYS.apiKey);
this.showSettings = false;
},
clearApiKey() {
this.apiKey = "";
this.apiKeyInput = "";
localStorage.removeItem(STORAGE_KEYS.apiKey);
this.showSettings = true;
},
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;
}
const frameCount = clamp(this.frameCount, 2, 24);
const fps = clamp(this.fps, 1, 24);
const prompt = this.userPrompt.trim();
this.frameCount = frameCount;
this.fps = fps;
this.resetOutput();
this.isGenerating = true;
try {
for (let i = 1; i <= frameCount; i++) {
this.progressText = `Generating frame ${i}/${frameCount}...`;
const image = await requestFrameImage({
apiKey: this.apiKey,
model: this.model,
userPrompt: prompt,
masterPrompt: this.masterPrompt,
frameCount,
frameIndex: i,
previousFrames: this.frames.slice(-2), // rolling window = 2
imageSize: this.imageSize,
aspectRatio: this.aspectRatio,
});
this.frames.push(image);
}
this.progressText = "Rendering GIF...";
this.gifBlob = await createGifFromFrames(this.frames, { fps });
this.gifUrl = URL.createObjectURL(this.gifBlob);
this.progressText = "Done ✨";
} catch (err) {
this.errorText = err?.message || "Generation failed.";
} finally {
this.isGenerating = false;
}
},
downloadGif() {
if (!this.gifUrl) return;
const a = document.createElement("a");
a.href = this.gifUrl;
a.download = `vibegif-${Date.now()}.gif`;
document.body.appendChild(a);
a.click();
a.remove();
},
refreshIcons() {
this.$nextTick(() => window.lucide?.createIcons());
},
};
}
let registered = false;
const register = () => {
if (registered) return;
registered = true;
window.Alpine.data("vibeGifApp", vibeGifApp);
};
if (window.Alpine) register();
document.addEventListener("alpine:init", register);