mirror of
https://github.com/vibegif/vibegif.lol.git
synced 2026-04-07 10:12:13 +00:00
Delete app.js
This commit is contained in:
362
app.js
362
app.js
@@ -1,362 +0,0 @@
|
||||
const $ = (s) => document.querySelector(s);
|
||||
const $$ = (s) => document.querySelectorAll(s);
|
||||
|
||||
const state = {
|
||||
apiKey: localStorage.getItem('vibegif_apikey') || '',
|
||||
generating: false,
|
||||
frames: [],
|
||||
gifUrl: null,
|
||||
aspectRatio: '1:1',
|
||||
workerBlob: null
|
||||
};
|
||||
|
||||
// --- Elements ---
|
||||
const elOnboarding = $('#section-onboarding');
|
||||
const elGenerator = $('#section-generator');
|
||||
const elModal = $('#modal-settings');
|
||||
const elBtnSettings = $('#btn-settings');
|
||||
const elBtnCloseSettings = $('#btn-close-settings');
|
||||
const elApiKeyOnboard = $('#input-apikey-onboard');
|
||||
const elApiKeyModal = $('#input-apikey-modal');
|
||||
const elPrompt = $('#input-prompt');
|
||||
const elModel = $('#select-model');
|
||||
const elFrames = $('#select-frames');
|
||||
const elFps = $('#select-fps');
|
||||
const elSize = $('#select-size');
|
||||
const elOptionHalfK = $('#option-half-k');
|
||||
const elBtnGenerate = $('#btn-generate');
|
||||
const elSectionFrames = $('#section-frames');
|
||||
const elFrameCounter = $('#frame-counter');
|
||||
const elFramesGrid = $('#frames-grid');
|
||||
const elSectionResult = $('#section-result');
|
||||
const elGifPreview = $('#gif-preview');
|
||||
const elBtnDownload = $('#btn-download');
|
||||
const elSectionError = $('#section-error');
|
||||
const elAspectButtons = $('#aspect-buttons');
|
||||
|
||||
// --- Init ---
|
||||
function init() {
|
||||
updateView();
|
||||
preloadWorker();
|
||||
lucide.createIcons();
|
||||
bindEvents();
|
||||
}
|
||||
|
||||
function updateView() {
|
||||
if (state.apiKey) {
|
||||
elOnboarding.classList.add('hidden');
|
||||
elGenerator.classList.remove('hidden');
|
||||
} else {
|
||||
elOnboarding.classList.remove('hidden');
|
||||
elGenerator.classList.add('hidden');
|
||||
}
|
||||
elApiKeyModal.value = state.apiKey;
|
||||
elApiKeyOnboard.value = state.apiKey;
|
||||
|
||||
// 0.5K visibility
|
||||
const isGemini = elModel.value === 'google/gemini-3.1-flash-image-preview';
|
||||
elOptionHalfK.style.display = isGemini ? '' : 'none';
|
||||
if (!isGemini && elSize.value === '0.5K') elSize.value = '1K';
|
||||
}
|
||||
|
||||
function saveApiKey(val) {
|
||||
state.apiKey = val;
|
||||
localStorage.setItem('vibegif_apikey', val);
|
||||
updateView();
|
||||
}
|
||||
|
||||
async function preloadWorker() {
|
||||
try {
|
||||
const r = await fetch('https://cdn.jsdelivr.net/npm/gif.js@0.2.0/dist/gif.worker.js');
|
||||
state.workerBlob = URL.createObjectURL(await r.blob());
|
||||
} catch (e) {
|
||||
console.error('Worker preload failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Events ---
|
||||
function bindEvents() {
|
||||
elBtnSettings.addEventListener('click', () => {
|
||||
elModal.classList.remove('hidden');
|
||||
elModal.classList.add('flex');
|
||||
});
|
||||
|
||||
elBtnCloseSettings.addEventListener('click', closeModal);
|
||||
elModal.addEventListener('click', (e) => {
|
||||
if (e.target === elModal) closeModal();
|
||||
});
|
||||
|
||||
elApiKeyOnboard.addEventListener('input', (e) => saveApiKey(e.target.value));
|
||||
elApiKeyModal.addEventListener('input', (e) => saveApiKey(e.target.value));
|
||||
|
||||
elModel.addEventListener('change', updateView);
|
||||
|
||||
$$('.ar-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
if (state.generating) return;
|
||||
$$('.ar-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
state.aspectRatio = btn.dataset.ar;
|
||||
});
|
||||
});
|
||||
|
||||
elBtnGenerate.addEventListener('click', generate);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
elModal.classList.add('hidden');
|
||||
elModal.classList.remove('flex');
|
||||
}
|
||||
|
||||
function setGenerating(val, statusText) {
|
||||
state.generating = val;
|
||||
const inputs = [elPrompt, elModel, elFrames, elFps, elSize];
|
||||
inputs.forEach(el => el.disabled = val);
|
||||
$$('.ar-btn').forEach(b => b.style.pointerEvents = val ? 'none' : '');
|
||||
|
||||
if (val) {
|
||||
elBtnGenerate.disabled = true;
|
||||
elBtnGenerate.classList.remove('bg-neutral-800', 'hover:bg-neutral-700');
|
||||
elBtnGenerate.classList.add('bg-neutral-100', 'text-neutral-400', 'cursor-not-allowed');
|
||||
elBtnGenerate.innerHTML = `
|
||||
<span class="flex items-center justify-center gap-2">
|
||||
<svg class="animate-spin h-4 w-4" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
|
||||
${statusText || 'generating...'}
|
||||
</span>`;
|
||||
} else {
|
||||
elBtnGenerate.disabled = false;
|
||||
elBtnGenerate.classList.add('bg-neutral-800', 'hover:bg-neutral-700');
|
||||
elBtnGenerate.classList.remove('bg-neutral-100', 'text-neutral-400', 'cursor-not-allowed');
|
||||
elBtnGenerate.innerHTML = 'generate gif';
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatus(text) {
|
||||
if (!state.generating) return;
|
||||
const span = elBtnGenerate.querySelector('span span') || elBtnGenerate.querySelector('span');
|
||||
if (span) {
|
||||
// Update just the text node
|
||||
const textNode = [...elBtnGenerate.querySelectorAll('span')].pop();
|
||||
if (textNode) textNode.textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
elSectionError.textContent = msg;
|
||||
elSectionError.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function hideError() {
|
||||
elSectionError.classList.add('hidden');
|
||||
elSectionError.textContent = '';
|
||||
}
|
||||
|
||||
// --- Frame Grid ---
|
||||
function renderFrameGrid() {
|
||||
const frameCount = parseInt(elFrames.value);
|
||||
elFramesGrid.innerHTML = '';
|
||||
elFrameCounter.textContent = `${state.frames.length}/${frameCount}`;
|
||||
elSectionFrames.classList.remove('hidden');
|
||||
|
||||
for (let i = 0; i < frameCount; i++) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'aspect-square rounded-lg overflow-hidden border border-neutral-200 bg-neutral-50 flex items-center justify-center';
|
||||
|
||||
if (i < state.frames.length) {
|
||||
const img = document.createElement('img');
|
||||
img.src = state.frames[i];
|
||||
img.className = 'w-full h-full object-cover';
|
||||
div.appendChild(img);
|
||||
} else if (i === state.frames.length && state.generating) {
|
||||
div.innerHTML = '<div class="w-5 h-5 border-2 border-neutral-300 border-t-neutral-600 rounded-full animate-spin"></div>';
|
||||
div.classList.add('border-dashed');
|
||||
} else {
|
||||
div.classList.add('border-dashed');
|
||||
}
|
||||
|
||||
elFramesGrid.appendChild(div);
|
||||
}
|
||||
}
|
||||
|
||||
// --- API ---
|
||||
async function callOpenRouter(messages) {
|
||||
const body = {
|
||||
model: elModel.value,
|
||||
messages,
|
||||
modalities: ['image'],
|
||||
image_config: {
|
||||
image_size: elSize.value,
|
||||
aspect_ratio: state.aspectRatio
|
||||
}
|
||||
};
|
||||
|
||||
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${state.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
'HTTP-Referer': 'https://vibegif.lol',
|
||||
'X-Title': 'vibegif.lol'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err?.error?.message || `API error ${res.status}`);
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
function extractImage(response) {
|
||||
const msg = response?.choices?.[0]?.message;
|
||||
if (!msg) return null;
|
||||
|
||||
// OpenRouter images array
|
||||
if (msg.images && msg.images.length > 0) {
|
||||
return msg.images[0]?.image_url?.url || null;
|
||||
}
|
||||
|
||||
// Fallback: content array with image parts
|
||||
if (Array.isArray(msg.content)) {
|
||||
for (const part of msg.content) {
|
||||
if (part.type === 'image_url') return part.image_url?.url || null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildRollingMessages(chatHistory, windowSize) {
|
||||
if (chatHistory.length <= 1 + windowSize * 2) return [...chatHistory];
|
||||
const first = chatHistory[0];
|
||||
const recent = chatHistory.slice(-(windowSize * 2));
|
||||
return [first, ...recent];
|
||||
}
|
||||
|
||||
// --- Generate ---
|
||||
async function generate() {
|
||||
const prompt = elPrompt.value.trim();
|
||||
const frameCount = parseInt(elFrames.value);
|
||||
if (!prompt || state.generating) return;
|
||||
|
||||
setGenerating(true, `generating frame 1/${frameCount}...`);
|
||||
state.frames = [];
|
||||
state.gifUrl = null;
|
||||
hideError();
|
||||
elSectionResult.classList.add('hidden');
|
||||
|
||||
const masterPrompt = `minimal black and white line doodle, single stroke, white background, kawaii style, ${prompt}`;
|
||||
|
||||
try {
|
||||
// Frame 1
|
||||
renderFrameGrid();
|
||||
const chatHistory = [
|
||||
{ role: 'user', content: masterPrompt }
|
||||
];
|
||||
|
||||
const res1 = await callOpenRouter(chatHistory);
|
||||
const img1 = extractImage(res1);
|
||||
if (!img1) throw new Error('No image returned for frame 1. Try a different prompt or model.');
|
||||
|
||||
state.frames.push(img1);
|
||||
renderFrameGrid();
|
||||
|
||||
chatHistory.push({
|
||||
role: 'assistant',
|
||||
content: [{ type: 'image_url', image_url: { url: img1 } }]
|
||||
});
|
||||
|
||||
// Subsequent frames
|
||||
for (let i = 2; i <= frameCount; i++) {
|
||||
updateStatus(`generating frame ${i}/${frameCount}...`);
|
||||
setGenerating(true, `generating frame ${i}/${frameCount}...`);
|
||||
|
||||
const nextPrompt = `imagine we are creating a ${frameCount} frame gif of "${prompt}". generate the next meaningful frame (frame ${i} of ${frameCount}). maintain the same minimal black and white line doodle, single stroke, white background, kawaii style.`;
|
||||
|
||||
chatHistory.push({ role: 'user', content: nextPrompt });
|
||||
|
||||
const rollingMessages = buildRollingMessages(chatHistory, 2);
|
||||
const resN = await callOpenRouter(rollingMessages);
|
||||
const imgN = extractImage(resN);
|
||||
if (!imgN) throw new Error(`No image returned for frame ${i}. Try reducing frame count or changing model.`);
|
||||
|
||||
state.frames.push(imgN);
|
||||
renderFrameGrid();
|
||||
|
||||
chatHistory.push({
|
||||
role: 'assistant',
|
||||
content: [{ type: 'image_url', image_url: { url: imgN } }]
|
||||
});
|
||||
}
|
||||
|
||||
// Build GIF
|
||||
setGenerating(true, 'assembling gif...');
|
||||
await buildGif();
|
||||
|
||||
} catch (e) {
|
||||
showError(e.message || 'Something went wrong');
|
||||
console.error(e);
|
||||
} finally {
|
||||
setGenerating(false);
|
||||
}
|
||||
}
|
||||
|
||||
// --- GIF Builder ---
|
||||
async function buildGif() {
|
||||
const fps = parseInt(elFps.value);
|
||||
const delay = Math.round(1000 / fps);
|
||||
|
||||
const images = await Promise.all(
|
||||
state.frames.map(src => new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = reject;
|
||||
img.src = src;
|
||||
}))
|
||||
);
|
||||
|
||||
const w = images[0].naturalWidth;
|
||||
const h = images[0].naturalHeight;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const gif = new GIF({
|
||||
workers: 2,
|
||||
quality: 10,
|
||||
width: w,
|
||||
height: h,
|
||||
workerScript: state.workerBlob || 'https://cdn.jsdelivr.net/npm/gif.js@0.2.0/dist/gif.worker.js',
|
||||
repeat: 0
|
||||
});
|
||||
|
||||
for (const img of images) {
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
ctx.drawImage(img, 0, 0, w, h);
|
||||
gif.addFrame(ctx, { copy: true, delay });
|
||||
}
|
||||
|
||||
gif.on('finished', (blob) => {
|
||||
state.gifUrl = URL.createObjectURL(blob);
|
||||
elGifPreview.src = state.gifUrl;
|
||||
elBtnDownload.href = state.gifUrl;
|
||||
elBtnDownload.download = `vibegif-${Date.now()}.gif`;
|
||||
elSectionResult.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
resolve();
|
||||
});
|
||||
|
||||
gif.on('error', reject);
|
||||
gif.render();
|
||||
});
|
||||
}
|
||||
|
||||
// --- Boot ---
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
Reference in New Issue
Block a user