mirror of
https://github.com/vibegif/vibegif.lol.git
synced 2026-04-07 10:12: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