Command Palette

Search for a command to run...

UnylyUnyly
Browse all

Pixel Artanim 8bit

FreeNot checked

PIXELANIM8 8-битный генератор GIF

GitHubEmbed

About

PIXELANIM8 8-битный генератор GIF

README

8-битный генератор GIF анимаций

Next.js TypeScript TailwindCSS Sharp


👤 Автор vs_lab

AI Engineer | FoodTech & Marketplaces | LLM, MLOps Проект создан с использованием AI-ассистента, который прошёл путь самопознания в нетривиальной задаче pixel-art генерации.

🧠 О проекте: AI-перспектива

Когда мне поставили задачу создать 8-битный генератор GIF, я не просто скопировал готовые решения. Я прошёл через несколько этапов "самопознания":

  1. Архитектурные решения — почему Sharp, а не Canvas? Почему gif-encoder-2, а не FFmpeg?
  2. Математика анимаций — каждый эффект требует уникальных алгоритмов
  3. Квантование цветов — как превратить 16 миллионов цветов в 8-битную палитру?
  4. Производительность — обработка пикселей в Node.js без блокировки event loop

Это нетривиальная задача, где каждый пиксель имеет значение.


🎯 Что умеет PixelAnim8

Входные данные

  • Drag & Drop загрузка изображений
  • Поддержка форматов: PNG, JPEG, GIF, BMP
  • Максимальный размер: 5MB

Анимации (математическое описание)

Эффект Алгоритм Математика
Pulse Изменение яркости brightness = 0.7 + sin(progress * 2π) * 0.3
Blink Периодическое затемнение Пороговый фильтр по фреймам
Shake Случайное смещение offset = round((random - 0.5) * 4)
Spin Вращение вокруг центра Матрица поворота: cos(θ), -sin(θ)
Bounce Прыжок по вертикали offset = floor(abs(sin(progress * 2π)) * 8)
Wave Волновое искажение wave = sin(y/height * 4π + progress * 2π) * 4
Zoom Масштабирование scale = 0.8 + sin(progress * 2π) * 0.2
Rainbow Цветовой сдвиг HSL rotate: hue = (h + progress * 1080) % 360

Цветовые палитры

// NES (Nintendo Entertainment System) - 8 цветов
nes: [
  [15, 15, 15],    // Черный
  [29, 43, 83],    // Темно-синий
  [126, 37, 83],   // Темно-фиолетовый
  [0, 135, 81],    // Темно-зеленый
  [171, 82, 54],   // Коричневый
  [95, 87, 79],    // Темно-серый
  [194, 195, 199], // Светло-серый
  [255, 241, 232]  // Белый
]

// Game Boy - 4 оттенка зелёного
gameboy: [[15, 56, 15], [48, 98, 48], [139, 172, 15], [155, 188, 15]]

// Commodore 64 - 8 характерных цветов
c64: [[0,0,0], [255,255,255], [136,0,0], [170,255,238], ...]

// Monochrome - классическая черно-белая
mono: [[0, 0, 0], [255, 255, 255]]

🏗️ Архитектура

src/
├── app/
│   ├── page.tsx          # Главная страница (Zustand state)
│   ├── layout.tsx        # Root layout с Google Fonts
│   ├── globals.css       # Retro CSS переменные
│   └── api/
│       └── generate/
│           └── route.ts  # API endpoint для генерации GIF
├── lib/
│   ├── types.ts          # TypeScript интерфейсы
│   └── store.ts          # Zustand store с localStorage persist
└── components/
    └── pixelanim8/
        ├── ImageUpload.tsx       # Drag & drop загрузка
        ├── PromptInput.tsx       # Ввод промпта
        ├── ParametersPanel.tsx   # Настройки палитры/размера
        ├── GenerateButton.tsx    # Кнопка генерации
        ├── ResultDisplay.tsx     # CRT-монитор с результатом
        └── HistoryGallery.tsx    # История генераций

Ключевые технологии

Технология Назначение Почему выбрана
Sharp Обработка изображений Быстрый нативный модуль, поддерживает resize, raw pixels
gif-encoder-2 Создание GIF Оптимизированный энкодер с поддержкой quantization
Zustand State management Минимальный boilerplate, встроенный persist
TailwindCSS 4 Стилизация CSS переменные для ретро-темы

🧮 Математика квантования цветов

// Евклидово расстояние в RGB пространстве
function findClosestColor(r: number, g: number, b: number, palette: number[][]): number[] {
  let minDist = Infinity;
  let closest = palette[0];
  
  for (const color of palette) {
    const dist = Math.sqrt(
      (r - color[0]) ** 2 + 
      (g - color[1]) ** 2 + 
      (b - color[2]) ** 2
    );
    if (dist < minDist) {
      minDist = dist;
      closest = color;
    }
  }
  return closest;
}

RGB ↔ HSL конверсия

Для эффекта Rainbow используется конвертация в HSL:

// RGB → HSL
function rgbToHsl(r: number, g: number, b: number): [number, number, number] {
  r /= 255; g /= 255; b /= 255;
  const max = Math.max(r, g, b), min = Math.min(r, g, b);
  let h = 0, s = 0;
  const l = (max + min) / 2;
  
  if (max !== min) {
    const d = max - min;
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
    switch (max) {
      case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
      case g: h = ((b - r) / d + 2) / 6; break;
      case b: h = ((r - g) / d + 4) / 6; break;
    }
  }
  return [h * 360, s, l];
}

// HSL → RGB (обратная конверсия)
function hslToRgb(h: number, s: number, l: number): [number, number, number] {
  // ... full implementation in route.ts
}

🚀 Как запускать

Development

# Установка зависимостей
bun install

# Запуск dev сервера
bun run dev

# Линтинг
bun run lint

Production Build

bun run build
bun run start

Деплой на Netlify

Проект включает готовую конфигурацию для Netlify:

netlify-app/
├── netlify.toml        # Конфигурация functions
├── netlify/functions/
│   └── generate.js     # Serverless function
└── public/
    └── index.html      # Статический фронтенд

🔧 Как бустить новыми фишками

1. Добавление новых анимаций

// В src/app/api/generate/route.ts

// 1. Добавь парсинг в parseAnimation()
function parseAnimation(prompt: string): string {
  const p = prompt.toLowerCase();
  // ... existing conditions
  if (p.includes('glitch')) return 'glitch';  // ← Новая анимация
  return 'default';
}

// 2. Реализуй эффект в createFrames()
case 'glitch': {
  // Алгоритм глитч-эффекта
  const glitched = Buffer.from(baseData);
  const sliceHeight = Math.floor(height * 0.1);
  const sliceY = Math.floor(Math.random() * (height - sliceHeight));
  const offset = Math.floor((Math.random() - 0.5) * 20);
  
  // Сдвиг горизонтальной полосы
  for (let y = sliceY; y < sliceY + sliceHeight; y++) {
    for (let x = 0; x < width; x++) {
      const srcX = (x + offset + width) % width;
      const srcIdx = (y * width + srcX) * 4;
      const dstIdx = (y * width + x) * 4;
      // ... copy pixels
    }
  }
  frames.push(glitched);
  continue;
}

2. Новые цветовые палитры

// Добавь в объект PALETTES
const PALETTES = {
  // ... existing palettes
  sega: [
    [0, 0, 0], [0, 0, 170], [0, 170, 0], [0, 170, 170],
    [170, 0, 0], [170, 0, 170], [170, 85, 0], [170, 170, 170],
    [85, 85, 85], [85, 85, 255], [85, 255, 85], [85, 255, 255],
    [255, 85, 85], [255, 85, 255], [255, 255, 85], [255, 255, 255]
  ],
  // Sega Master System - 16 цветов
};

3. Математические эффекты для добавления

// === ПОТОКОПИКСЕЛЬНАЯ ОТРИСОВКА ===
// Пиксели появляются один за другим
case 'pixel-reveal': {
  const totalPixels = width * height;
  const visiblePixels = Math.floor(progress * totalPixels);
  
  const revealed = Buffer.alloc(frame.length);
  for (let i = 0; i < visiblePixels * 4; i++) {
    revealed[i] = baseData[i];
  }
  frames.push(revealed);
  continue;
}

// === ЦВЕТОВАЯ ВОЛНА ===
// Прорисовка по цветам (сортировка по hue)
case 'color-wave': {
  const sortedPixels: Array<{x: number, y: number, hue: number}> = [];
  
  // Сортировка пикселей по hue
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      const idx = (y * width + x) * 4;
      const [h] = rgbToHsl(baseData[idx], baseData[idx+1], baseData[idx+2]);
      sortedPixels.push({x, y, hue: h});
    }
  }
  sortedPixels.sort((a, b) => a.hue - b.hue);
  
  const waveProgress = Math.floor(progress * sortedPixels.length);
  const wavedFrame = Buffer.alloc(frame.length);
  
  for (let i = 0; i < waveProgress; i++) {
    const {x, y} = sortedPixels[i];
    const idx = (y * width + x) * 4;
    wavedFrame[idx] = baseData[idx];
    wavedFrame[idx + 1] = baseData[idx + 1];
    wavedFrame[idx + 2] = baseData[idx + 2];
    wavedFrame[idx + 3] = 255;
  }
  frames.push(wavedFrame);
  continue;
}

// === ГРАНИЧНАЯ АНИМАЦИЯ (по соседним пикселям) ===
// Распространение от краёв к центру
case 'edge-scan': {
  const distanceFromEdge = Math.min(progress * Math.max(width, height), Math.max(width, height));
  
  const scanned = Buffer.alloc(frame.length);
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      const distToEdge = Math.min(x, y, width - x - 1, height - y - 1);
      const srcIdx = (y * width + x) * 4;
      
      if (distToEdge <= distanceFromEdge) {
        scanned[srcIdx] = baseData[srcIdx];
        scanned[srcIdx + 1] = baseData[srcIdx + 1];
        scanned[srcIdx + 2] = baseData[srcIdx + 2];
        scanned[srcIdx + 3] = 255;
      }
    }
  }
  frames.push(scanned);
  continue;
}

// === ФРАКТАЛЬНЫЙ ШУМ ===
case 'noise-field': {
  const noiseScale = 0.05;
  const timeOffset = progress * 10;
  
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      const idx = (y * width + x) * 4;
      const noise = Math.sin(x * noiseScale + timeOffset) * 
                    Math.cos(y * noiseScale + timeOffset);
      const intensity = 0.8 + noise * 0.2;
      
      frame[idx] = Math.min(255, Math.floor(baseData[idx] * intensity));
      frame[idx + 1] = Math.min(255, Math.floor(baseData[idx + 1] * intensity));
      frame[idx + 2] = Math.min(255, Math.floor(baseData[idx + 2] * intensity));
    }
  }
  break;
}

📊 Производительность

Оптимизации

  1. Buffer вместо TypedArrays — прямая работа с памятью
  2. Параллельная обработка — Sharp использует libvips
  3. Минимальные аллокации — переиспользование буферов

Метрики

Размер Кадров Время генерации
128px 4 ~200ms
256px 8 ~500ms
512px 16 ~1.5s

🎨 UI/UX особенности

Retro-стилизация

/* Пиксельная сетка фона */
.pixel-grid-bg {
  background-image: 
    repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255, 204, 0, 0.03) 2px),
    repeating-linear-gradient(90deg, transparent, transparent 2px, rgba(255, 204, 0, 0.03) 2px);
}

/* CRT-монитор */
.monitor-frame {
  background: linear-gradient(145deg, #8b8b8b 0%, #6b6b6b 50%, #8b8b8b 100%);
  box-shadow: inset 0 2px 4px rgba(255,255,255,0.3), 
              inset 0 -2px 4px rgba(0,0,0,0.3), 
              0 8px 32px rgba(0,0,0,0.5);
}

.monitor-screen {
  box-shadow: inset 0 0 20px rgba(0, 255, 0, 0.1), 
              inset 0 0 60px rgba(0, 0, 0, 0.8);
}

Шрифты

  • Press Start 2P — заголовки, кнопки (пиксельный шрифт)
  • VT323 — основной текст (терминальный шрифт)

🔌 API Endpoints

POST /api/generate

// Request
{
  image: string,      // Base64 data URL
  prompt: string,     // Natural language description
  params: {
    palette: 'nes' | 'gameboy' | 'c64' | 'mono',
    frames: number,   // 1-16
    delay: number,    // FPS (1-30)
    size: '128' | '256' | '512'
  }
}

// Response
{
  success: boolean,
  gifData?: string,   // Base64 GIF data URL
  metadata?: {
    frameCount: number,
    palette: string,
    fileSize: number,
    dimensions: { width: number, height: number }
  },
  error?: string
}

📦 Зависимости

{
  "dependencies": {
    "sharp": "^0.33.x",           // Image processing
    "gif-encoder-2": "^1.0.x",    // GIF generation
    "zustand": "^4.5.x",          // State management
    "next": "^16.x",              // Framework
    "react": "^19.x",             // UI
    "tailwindcss": "^4.x"         // Styling
  }
}

🛠️ Roadmap улучшений архитектуры

1. WebWorker для обработки

// worker.ts
self.onmessage = (e) => {
  const { baseData, width, height, animationType, frameCount } = e.data;
  const frames = createFrames(baseData, width, height, animationType, frameCount);
  self.postMessage({ frames });
};

2. Кэширование результатов

import { LRUCache } from 'lru-cache';

const cache = new LRUCache<string, Buffer>({
  max: 100,
  ttl: 1000 * 60 * 60, // 1 hour
});

// Cache key = hash(image + prompt + params)

3. Streaming response

// Для больших GIF
const stream = new ReadableStream({
  async pull(controller) {
    for (const frame of frames) {
      controller.enqueue(frame);
    }
    controller.close();
  }
});

4. WebSocket прогресс

// Реал-тайм обновления прогресса
io.on('connection', (socket) => {
  socket.on('generate', async (data) => {
    for (let i = 0; i < frameCount; i++) {
      const frame = await createFrame(i);
      socket.emit('progress', { frame: i, total: frameCount });
    }
  });
});

📝 Лицензия

MIT License - используй как хочешь, создавай ретро-арт!


🙏 Благодарности

  • Nintendo — за эстетику NES
  • Nintendo — за культовый Game Boy
  • Commodore — за C64 и его легендарную палитру
  • Sharp и gif-encoder-2 — за инструменты

Made with ❤️ for retro gaming fans
2024 PixelAnim8


🤖 AI Note

Этот проект — результат коллаборации человека и AI. Каждый пиксель, каждый алгоритм, каждая CSS-переменная были созданы через диалог, итерации и debug. Нетривиальная задача генерации pixel-art анимаций потребовала:

  • Архитектурного мышления — выбор правильных инструментов
  • Математической точности — формулы анимаций и квантования
  • Эстетического чувства — ретро-стилизация UI
  • Отладки — исправление OOM, CSS import order, gif-encoder API

Generated with 💛 by AI Engineer | FoodTech & Marketplaces | LLM, MLOps

from github.com/vlad-Stujhev1/pixel-artanim-8bit

Installing Pixel Artanim 8bit

This server has no published package — it is built from source. Open the repository and follow its README.

▸ github.com/vlad-Stujhev1/pixel-artanim-8bit

FAQ

Is Pixel Artanim 8bit MCP free?

Yes, Pixel Artanim 8bit MCP is free — one-click install via Unyly at no cost.

Does Pixel Artanim 8bit need an API key?

No, Pixel Artanim 8bit runs without API keys or environment variables.

Is Pixel Artanim 8bit hosted or self-hosted?

Self-hosted: the server runs locally on your machine via the install command above.

How do I install Pixel Artanim 8bit in Claude Desktop, Claude Code or Cursor?

Open Pixel Artanim 8bit on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.

Related MCPs

Compare Pixel Artanim 8bit with

Not sure what to pick?

Find your stack in 60 seconds

Author?

Embed badge for your README

Browse similar

All ai MCPs