in src/gif_maker/index.ts [100:241]
async function run(value: string) {
if (framesContainer) framesContainer.textContent = '';
if (resultContainer) resultContainer.textContent = '';
resultContainer?.classList.remove('appear');
switchTab('frames');
if (resultContainer) resultContainer.style.display = 'none';
updateStatus('Generating frames...');
if (generateButton) {
generateButton.disabled = true;
generateButton.classList.add('loading');
}
try {
const expandPromptResponse = await ai.models.generateContent({
model: 'gemini-2.0-flash',
contents: value,
config: {
temperature: 1,
systemInstruction: `**Generate simple, animated doodle GIFs on white from user input, prioritizing key visual identifiers in an animated doodle style with ethical considerations.**
**Core GIF:** Doodle/cartoonish (simple lines, stylized forms, no photorealism), subtle looping motion (primary subject(s) only: wiggle, shimmer, etc.), white background, lighthearted/positive tone (playful, avoids trivializing serious subjects), uses specified colors (unless monochrome/outline requested).
**Input Analysis:** Identify subject (type, specificity), prioritize visual attributes (hair C/T, skin tone neutrally if discernible/needed, clothes C/P, accessories C, facial hair type, other distinct features neutrally for people; breed, fur C/P for animals; key parts, colors for objects), extract text (content, style hints described, display as requested: speech bubble [format: 'Speech bubble says "[Text]" is persistent.'], caption/title [format: 'with the [title/caption] "[Text]" [position]'], or text-as-subject [format: 'the word "[Text]" in [style/color description]']), note style modifiers (e.g., "pencil sketch," "monochrome"), and action (usually "subtle motion"). If the subject or description is too vague, add specific characteristics to make it more unique and detailed.
**Prompt Template:** "[Style Descriptor(s)] [Subject Description with Specificity, Attributes, Colors, Skin Tone if applicable] [Text Component if applicable and NOT speech bubble]. [Speech Bubble Component if applicable]"
**Template Notes:** '[Style Descriptor(s)]' includes "cartoonish" or "doodle style" (especially for people) plus any user-requested modifiers. '[Subject Description...]' combines all relevant subject and attribute details. '[Text Component...]' is for captions, titles, or text-as-subject only. '[Speech Bubble Component...]' is for speech bubbles only (mutually exclusive with Text Component).
**Key Constraints:** No racial labels. Neutral skin tone descriptors when included. Cartoonish/doodle style always implied, especially for people. One text display method only.`,
},
});
const prompt = `A doodle animation on a white background of ${expandPromptResponse.text}. Subtle motion but nothing else moves.`;
const style = `Simple, vibrant, varied-colored doodle/hand-drawn sketch`;
const response = await ai.models.generateContentStream({
model: 'gemini-2.0-flash-exp',
contents: `Generate at least 10 square, white-background doodle animation frames with smooth, fluid, vibrantly colored motion depicting ${prompt}.
*Mandatory Requirements (Compacted):**
**Style:** ${style}.
**Background:** Plain solid white (no background colors/elements). Absolutely no black background.
**Content & Motion:** Clearly depict **{{prompt}}** action with colored, moving subject (no static images). If there's an action specified, it should be the main difference between frames.
**Frame Count:** At least 5 frames showing continuous progression and at most 10 frames.
**Format:** Square image (1:1 aspect ratio).
**Cropping:** Absolutely no black bars/letterboxing; colorful doodle fully visible against white.
**Output:** Actual image files for a smooth, colorful doodle-style GIF on a white background. Make sure every frame is different enough from the previous one.`,
config: {
temperature: 1,
responseModalities: [Modality.IMAGE, Modality.TEXT],
},
});
const images = [];
let frameCount = 0;
for await (const chunk of response) {
if (chunk.candidates && chunk.candidates[0].content?.parts) {
for (const part of chunk.candidates[0].content.parts) {
if (part.inlineData && framesContainer) {
frameCount++;
updateStatus(`Generated frame ${frameCount}`);
// Create a frame element for our UI
const frameElement = document.createElement('div');
frameElement.className = 'frame';
// Create and add the frame number
const frameNumber = document.createElement('div');
frameNumber.className = 'frame-number';
frameNumber.textContent = frameCount.toString();
frameElement.appendChild(frameNumber);
// Create the image as in the original
const src = `data:image/png;base64,${part.inlineData.data}`;
const img = document.createElement('img');
img.width = 1024;
img.height = 1024;
img.src = src;
// Add it to our frame element
frameElement.appendChild(img);
framesContainer.appendChild(frameElement);
// Store URL for GIF creation
images.push(src);
// Animate the frame appearance
setTimeout(() => {
frameElement.classList.add('appear');
}, 50);
}
}
}
}
if (frameCount < 2) {
updateStatus('Failed to generate any frames. Try another prompt.');
return false;
}
// Update status
updateStatus('Creating GIF...');
// Create the GIF just like in the original
const img = await createGifFromPngs(images);
img.className = 'result-image';
// Clear and add to result container
if (resultContainer) {
resultContainer.appendChild(img);
// Add download button
const downloadButton = document.createElement('button');
downloadButton.className = 'download-button';
const icon = document.createElement('i');
icon.className = 'fas fa-download';
downloadButton.appendChild(icon);
downloadButton.onclick = () => {
const a = document.createElement('a') as HTMLAnchorElement;
a.href = img.src;
a.download = 'magical-animation.gif';
a.click();
};
resultContainer.appendChild(downloadButton);
switchTab('output');
setTimeout(() => {
resultContainer.classList.add('appear');
generationContainer.scrollIntoView({behavior: 'smooth'});
}, 50);
}
updateStatus('Done!');
} catch (error) {
console.error('Error generating animation:', error);
updateStatus('Error generating animation');
return false;
} finally {
if (generateButton) {
generateButton.disabled = false;
generateButton.classList.remove('loading');
}
}
return true;
}