# 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<RgbaColor> Palette { get; }     // shared by all layers
    public List<MinintLayer> 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<MinintDocument> 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<MinintDocument>
      ├─ Name, FrameDelayMs
      ├─ Palette: List<RgbaColor>      ← shared by layers
      └─ Layers: List<MinintLayer>
           ├─ 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<int>`.
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.
