# Minint — Architecture Document (Stage 1) ## 1. Solution Structure ``` Minint.slnx ├── Minint.Core/ — Domain models, interfaces, pure logic (no UI, no I/O) │ └── Minint.Core.csproj (net10.0, classlib) ├── Minint.Infrastructure/ — Binary serialization, BMP/GIF export │ └── Minint.Infrastructure.csproj (net10.0, classlib, references Core) ├── Minint/ — Avalonia UI application │ └── Minint.csproj (net10.0, WinExe, references Core + Infrastructure) └── Minint.Tests/ — Unit tests (added later if needed) └── Minint.Tests.csproj (net10.0, xunit, references Core + Infrastructure) ``` ### Dependency graph ``` Minint (UI) ──► Minint.Core │ └──────► Minint.Infrastructure ──► Minint.Core ``` UI depends on Core and Infrastructure. Infrastructure depends on Core only. Core has zero external dependencies (pure domain). --- ## 2. RAM Model (Domain Objects) ### 2.1 RgbaColor ```csharp public readonly record struct RgbaColor(byte R, byte G, byte B, byte A) { public static readonly RgbaColor Transparent = new(0, 0, 0, 0); } ``` Value type. Equality by all four channels (record struct gives this for free). ### 2.2 MinintLayer ```csharp public sealed class MinintLayer { public string Name { get; set; } public bool IsVisible { get; set; } public byte Opacity { get; set; } // 0–255, reserved for future blending public int[] Pixels { get; } // length = Width * Height, row-major } ``` - `Pixels[y * width + x]` = index into the parent document's palette. - `int` chosen over `uint` because C# array indexing is int-based; max value 2³¹−1 is more than sufficient. - On-disk packing (1–4 bytes per index) is handled only by the serializer. ### 2.3 MinintDocument ```csharp public sealed class MinintDocument { public string Name { get; set; } public uint FrameDelayMs { get; set; } // animation delay public List Palette { get; } // shared by all layers public List Layers { get; } } ``` - Palette is per-document, shared by all layers of that document. - Index 0 is **always** `RgbaColor.Transparent` by convention (the eraser writes 0). - When a new document is created, the palette is initialized with at least `[Transparent]`. ### 2.4 MinintContainer ```csharp public sealed class MinintContainer { public int Width { get; set; } public int Height { get; set; } public List Documents { get; } } ``` - Width/Height at the container level: all documents and all layers share the same dimensions. This is required for coherent animation playback and GIF export. ### 2.5 Diagram ``` MinintContainer ├─ Width, Height └─ Documents: List ├─ Name, FrameDelayMs ├─ Palette: List ← shared by layers └─ Layers: List ├─ Name, IsVisible, Opacity └─ Pixels: int[] ← indices into Palette ``` --- ## 3. Binary Format `.minint` All multi-byte integers are **little-endian**. Strings are **UTF-8**, prefixed by a 1-byte length (max 255 chars for names). ### 3.1 Container Header (28 bytes fixed) | Offset | Size | Type | Description | |--------|-------|---------|--------------------------------| | 0 | 6 | ASCII | Signature: `MININT` | | 6 | 2 | uint16 | Format version (currently `1`) | | 8 | 4 | uint32 | Width | | 12 | 4 | uint32 | Height | | 16 | 4 | uint32 | DocumentCount | | 20 | 8 | — | Reserved (must be zero) | ### 3.2 Document Block (repeated DocumentCount times) #### Document Header | Offset (relative) | Size | Type | Description | |--------------------|-------------|---------|------------------------| | 0 | 1 | uint8 | NameLength | | 1 | NameLength | UTF-8 | Name | | 1+NameLength | 4 | uint32 | FrameDelayMs | | 5+NameLength | 4 | uint32 | PaletteCount | #### Palette (immediately follows) `PaletteCount × 4` bytes: each color is `[R, G, B, A]` (1 byte each). #### Index Byte Width Derived from PaletteCount (not stored explicitly — reader computes it): | PaletteCount | BytesPerIndex | |----------------------|---------------| | 1 – 255 | 1 | | 256 – 65 535 | 2 | | 65 536 – 16 777 215 | 3 | | 16 777 216+ | 4 | #### Layer Count | Size | Type | Description | |------|--------|-------------| | 4 | uint32 | LayerCount | #### Layer Block (repeated LayerCount times) | Size | Type | Description | |-------------------|--------|--------------------------------------| | 1 | uint8 | LayerNameLength | | LayerNameLength | UTF-8 | LayerName | | 1 | uint8 | IsVisible (0 = hidden, 1 = visible) | | 1 | uint8 | Opacity (0–255) | | W×H×BytesPerIndex | bytes | Pixel indices, row-major, LE per idx | ### 3.3 Validation Rules 1. Signature must be exactly `MININT`. 2. Version must be `1` (future-proof: reject unknown versions). 3. Width, Height ≥ 1. 4. DocumentCount ≥ 1. 5. PaletteCount ≥ 1 (at least the transparent color). 6. Every pixel index must be `< PaletteCount`. 7. Reserved bytes must be zero (warn on non-zero for forward compat). 8. Unexpected EOF → error with clear message. ### 3.4 Format Size Estimate For a 64×64 image, 1 document, 1 layer, 16-color palette: - Header: 28 bytes - Doc header: ~10 bytes - Palette: 16 × 4 = 64 bytes - Layer header: ~8 bytes - Pixel data: 64 × 64 × 1 = 4 096 bytes - **Total ≈ 4.2 KB** --- ## 4. Classes, Interfaces, and Services ### 4.1 Core — Domain Models | Type | Kind | Responsibility | |---------------------|---------------|-----------------------------------------| | `RgbaColor` | record struct | Immutable RGBA color value | | `MinintLayer` | class | Single raster layer (index buffer) | | `MinintDocument` | class | Document with palette + layers | | `MinintContainer` | class | Top-level container of documents | ### 4.2 Core — Interfaces | Interface | Methods (key) | |------------------------|--------------------------------------------------| | `ICompositor` | `uint[] Composite(MinintDocument, int w, int h)` | | `IPaletteService` | `int EnsureColor(doc, RgbaColor)`, `void CompactPalette(doc)` | | `IDrawingService` | `void ApplyBrush(layer, x, y, radius, colorIdx, w, h)` | | `IFloodFillService` | `void Fill(layer, x, y, newColorIdx, w, h)` | | `IImageEffectService` | `void AdjustContrast(doc, float factor)`, `void ToGrayscale(doc)` | | `IPatternGenerator` | `MinintDocument Generate(PatternParams)` | | `IFragmentService` | `void CopyFragment(src, dst, rect, destPoint)` | ### 4.3 Core — Service Implementations | Class | Implements | Notes | |------------------------|-------------------------|------------------------------------| | `Compositor` | `ICompositor` | Alpha-blends layers bottom→top | | `PaletteService` | `IPaletteService` | Color lookup, add, compact | | `DrawingService` | `IDrawingService` | Circle mask brush/eraser | | `FloodFillService` | `IFloodFillService` | BFS flood fill on index array | | `ImageEffectService` | `IImageEffectService` | Palette-based contrast/grayscale | | `PatternGenerator` | `IPatternGenerator` | Checkerboard, gradient, stripes… | | `FragmentService` | `IFragmentService` | Copy rect with palette merging | ### 4.4 Infrastructure | Class | Responsibility | |-------------------------|---------------------------------------------------| | `MinintSerializer` | Read/write `.minint` binary format | | `BmpExporter` | Export composited document to 32-bit BMP | | `GifExporter` | Export all documents as animated GIF (uses lib) | ### 4.5 UI (Avalonia) — Key Types | Type | Kind | Responsibility | |----------------------------|------------|----------------------------------------| | `MainWindowViewModel` | ViewModel | Orchestrates the editor | | `EditorViewModel` | ViewModel | Current document, tool state, viewport | | `DocumentListViewModel` | ViewModel | Document tabs / frame list | | `LayerListViewModel` | ViewModel | Layer panel | | `ToolSettingsViewModel` | ViewModel | Brush size, color, tool type | | `PixelCanvas` | Control | Custom Avalonia control for rendering | | `Viewport` | Model | Zoom, offset, coord transforms | --- ## 5. Key Algorithms ### 5.1 Compositing (layers → RGBA buffer) ``` result = new uint[W * H] (RGBA packed as uint, initialized to transparent) for each layer (bottom → top): if !layer.IsVisible: skip for each pixel i in 0..W*H-1: srcColor = palette[layer.Pixels[i]] effectiveAlpha = srcColor.A * layer.Opacity / 255 if effectiveAlpha == 0: continue result[i] = AlphaBlend(result[i], srcColor with A=effectiveAlpha) ``` Alpha blending (standard "over" operator): ``` outA = srcA + dstA * (255 - srcA) / 255 outR = (srcR * srcA + dstR * dstA * (255 - srcA) / 255) / outA (same for G, B) ``` ### 5.2 Brush / Eraser Circle mask: for each pixel (px, py) within bounding box of (cx ± r, cy ± r): ``` if (px - cx)² + (py - cy)² <= r²: mark pixel as affected ``` - Brush: writes `colorIndex` to affected pixels. - Eraser: writes `0` (transparent index) to affected pixels. - Preview: compute mask, overlay on canvas as semi-transparent highlight without modifying pixel data. ### 5.3 Flood Fill (BFS) ``` targetIdx = layer.Pixels[y * W + x] if targetIdx == fillIdx: return queue = { (x, y) } visited = bitset of W * H while queue not empty: (cx, cy) = dequeue if out of bounds or visited: continue if layer.Pixels[cy * W + cx] != targetIdx: continue mark visited layer.Pixels[cy * W + cx] = fillIdx enqueue 4-neighbors ``` ### 5.4 Palette Compaction On save (or on demand): 1. Scan all layers, collect used indices into a `HashSet`. 2. Build new palette = only used colors (keep index 0 = transparent). 3. Build old→new index mapping. 4. Remap all pixel arrays. ### 5.5 Copy Fragment Between Documents (A4) ``` for each pixel (sx, sy) in source rect: srcIdx = srcLayer.Pixels[sy * W + sx] srcColor = srcDoc.Palette[srcIdx] dstIdx = dstDoc.PaletteService.EnsureColor(srcColor) dstLayer.Pixels[destY * W + destX] = dstIdx destX++; ... ``` `EnsureColor` checks if color exists in target palette; if not, appends it and returns the new index. ### 5.6 Contrast (A1) Applied to palette (not individual pixels): ``` for each color in palette (skip index 0 transparent): R' = clamp(factor * (R - 128) + 128, 0, 255) G' = clamp(factor * (G - 128) + 128, 0, 255) B' = clamp(factor * (B - 128) + 128, 0, 255) A' = A (unchanged) ``` `factor > 1` = increase contrast, `factor < 1` = decrease. This is correct because all pixels reference the palette; modifying palette entries globally changes the image. No pixel remapping needed. **Caveat**: if two palette entries become identical after transformation, they could be merged (optional optimization). ### 5.7 Grayscale (A2) Applied to palette: ``` for each color in palette (skip index 0 transparent): gray = (byte)(0.299 * R + 0.587 * G + 0.114 * B) color = RgbaColor(gray, gray, gray, A) ``` Uses ITU-R BT.601 luminance formula (standard for perceptual grayscale). Same caveat about duplicate entries applies. ### 5.8 BMP Export 1. Composite document into RGBA buffer. 2. Write BMP file header (14 bytes) + DIB BITMAPINFOHEADER (40 bytes). 3. Write pixel data as 32-bit BGRA, bottom-up row order. 4. No compression, no palette section (direct 32-bit). ### 5.9 GIF Export 1. For each document in container, composite into RGBA buffer. 2. Quantize each frame to 256 colors (popularity-based, with transparent slot). 3. LZW-compress and write as GIF89a with per-frame delay from `FrameDelayMs`. 4. NETSCAPE2.0 application extension for infinite looping. Fully self-implemented — no external library for GIF encoding. ### 5.10 Pattern Generation (Б4) Factory-based: `PatternType` enum + parameters struct per pattern type. | Pattern | Parameters | |----------------------|-------------------------------------| | Checkerboard | cellSize, color1, color2 | | Gradient (H/V) | startColor, endColor, direction | | Stripes (H/V) | stripeWidth, color1, color2 | | Concentric Circles | centerX, centerY, ringWidth, colors | | Tile | tileWidth, tileHeight, colors | Each generator creates a `MinintDocument` with an appropriate palette and a single layer. --- ## 6. Viewport and Canvas Design ``` Viewport: Zoom: double (1.0 = 1 pixel = 1 screen pixel) OffsetX, OffsetY: double (pan position) ScreenToPixel(screenX, screenY) → (pixelX, pixelY) pixelX = (int)floor((screenX - OffsetX) / Zoom) pixelY = (int)floor((screenY - OffsetY) / Zoom) PixelToScreen(pixelX, pixelY) → (screenX, screenY) screenX = pixelX * Zoom + OffsetX screenY = pixelY * Zoom + OffsetY ``` Canvas (`PixelCanvas` custom control): - Renders composited bitmap using `DrawImage` with `BitmapInterpolationMode.None` (nearest-neighbor). - Overlays pixel grid (vertical + horizontal lines) when zoom > threshold and grid enabled. - Overlays brush/eraser preview as semi-transparent mask. - Handles mouse events: wheel → zoom, middle-drag → pan, left-click/drag → tool action. --- ## 7. Key Decisions and Trade-offs ### D1. Width/Height at container level **Chosen**: container-level (all documents share dimensions). **Rationale**: required for coherent animation; GIF frames must be same size. **Trade-off**: cannot mix different-sized documents in one container. ### D2. int[] for pixel indices in RAM **Chosen**: `int[]` (4 bytes per pixel in memory). **Rationale**: simple, fast, native array indexing; on-disk packing handles storage efficiency. **Trade-off**: memory usage is 4× compared to byte[] for small palettes, but acceptable for typical image sizes (e.g., 1024×1024 = 4 MB). ### D3. Transparent color at palette index 0 **Chosen**: convention, always enforced. **Rationale**: simplifies eraser (write 0), new layer initialization (fill with 0), and compositing (skip transparent early). **Trade-off**: first palette slot is reserved; user cannot reassign it. ### D4. Grayscale / contrast via palette transformation **Chosen**: modify palette entries directly. **Rationale**: elegant — all pixels update automatically; no per-pixel remapping. **Trade-off**: operation is document-wide (affects all layers). Per-layer effects would require palette duplication or a different approach. For this project scope, document-wide is acceptable and explicitly chosen. ### D5. Layer opacity in compositing **Chosen**: multiply palette alpha by layer opacity byte. **Rationale**: opens path for future layer blending without breaking anything now. **Default**: new layers have `Opacity = 255` (fully opaque). ### D6. Eraser semantics **Chosen**: eraser writes index 0 (transparent). **Rationale**: consistent with palette convention; compositing naturally handles it. ### D7. GIF export **Chosen**: fully self-implemented GIF89a encoder with LZW compression. **Rationale**: avoids external dependencies; the entire project uses only Avalonia and CommunityToolkit.Mvvm. **Details**: popularity-based color quantization to 256 colors, NETSCAPE2.0 looping extension. ### D8. No undo/redo in initial implementation **Chosen**: not implemented in first pass; architecture does not prevent it. **Path forward**: command pattern or snapshot-based undo can be layered on later. --- ## 8. Staged Implementation Plan | Stage | Scope | |-------|----------------------------------------------------| | 1 | ✅ Architecture & design (this document) | | 2 | ✅ Solution scaffold + domain models | | 3 | ✅ Binary `.minint` serialization + round-trip test | | 4 | ✅ Compositing + palette management + RGBA buffer | | 5 | ✅ Basic Avalonia UI (main window, menus, panels) | | 6 | ✅ Canvas: pan, zoom, grid, nearest-neighbor | | 7 | ✅ Drawing tools: brush, eraser, flood fill, preview | | 8 | ✅ Layer & document management UI | | 9 | ✅ Effects: contrast, grayscale, fragment copy, patterns | | 10 | ✅ Animation playback + BMP/GIF export | | 11 | ✅ Polish, tests, documentation | --- ## 9. External Dependencies | Package | Project | Purpose | |---------------------------------|----------------|-----------------------| | Avalonia 11.3.8 | Minint (UI) | UI framework | | Avalonia.Desktop 11.3.8 | Minint (UI) | Desktop host | | Avalonia.Themes.Fluent 11.3.8 | Minint (UI) | Theme | | CommunityToolkit.Mvvm 8.2.1 | Minint (UI) | MVVM helpers | | _(no external deps)_ | Infrastructure | GIF/BMP fully self-implemented | Core project: **zero** external dependencies.