Files
Minint/ARCHITECTURE.md
2026-03-29 15:34:33 +03:00

18 KiB
Raw Blame History

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

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

public sealed class MinintLayer
{
    public string Name { get; set; }
    public bool IsVisible { get; set; }
    public byte Opacity { get; set; }       // 0255, 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 (14 bytes per index) is handled only by the serializer.

2.3 MinintDocument

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

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 (0255)
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. Use external library (e.g. SixLabors.ImageSharp) to encode frames with per-frame delay from FrameDelayMs.
  3. Output as animated GIF.

Only the GIF encoder is external; the .minint format is fully self-implemented.

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 library

Chosen: external library for GIF encoding only (SixLabors.ImageSharp or equivalent). Rationale: GIF LZW compression is complex and not the focus of this project. Constraint: .minint serialization remains fully self-implemented.

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
SixLabors.ImageSharp (TBD) Infrastructure GIF export only

Core project: zero external dependencies.