mirror of
https://github.com/vibegif/vibegif.lol.git
synced 2026-04-07 18:22:13 +00:00
Feat: Alpine state + generation loop
This commit is contained in:
198
assets/js/app.js
Normal file
198
assets/js/app.js
Normal file
@@ -0,0 +1,198 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user