From 08b9039b58e0845163b214d7edaac7248f7c60f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=8B=D1=82=D0=BA=D0=BE=D0=B2=20=D0=A0=D0=BE=D0=BC?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Sun, 29 Mar 2026 15:34:33 +0300 Subject: [PATCH] =?UTF-8?q?=D0=AD=D1=82=D0=B0=D0=BF=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ARCHITECTURE.md | 482 ++++++++++++++++++ Minint.Core/Minint.Core.csproj | 9 + Minint.Core/Models/MinintContainer.cs | 35 ++ Minint.Core/Models/MinintDocument.cs | 53 ++ Minint.Core/Models/MinintLayer.cs | 42 ++ Minint.Core/Models/RgbaColor.cs | 37 ++ Minint.Core/Services/IBmpExporter.cs | 13 + Minint.Core/Services/ICompositor.cs | 13 + Minint.Core/Services/IDrawingService.cs | 25 + Minint.Core/Services/IFloodFillService.cs | 12 + Minint.Core/Services/IFragmentService.cs | 29 ++ Minint.Core/Services/IGifExporter.cs | 13 + Minint.Core/Services/IImageEffectService.cs | 20 + Minint.Core/Services/IMinintSerializer.cs | 17 + Minint.Core/Services/IPaletteService.cs | 23 + Minint.Core/Services/IPatternGenerator.cs | 28 + .../Minint.Infrastructure.csproj | 13 + Minint.slnx | 2 + Minint/Minint.csproj | 9 +- 19 files changed, 872 insertions(+), 3 deletions(-) create mode 100644 ARCHITECTURE.md create mode 100644 Minint.Core/Minint.Core.csproj create mode 100644 Minint.Core/Models/MinintContainer.cs create mode 100644 Minint.Core/Models/MinintDocument.cs create mode 100644 Minint.Core/Models/MinintLayer.cs create mode 100644 Minint.Core/Models/RgbaColor.cs create mode 100644 Minint.Core/Services/IBmpExporter.cs create mode 100644 Minint.Core/Services/ICompositor.cs create mode 100644 Minint.Core/Services/IDrawingService.cs create mode 100644 Minint.Core/Services/IFloodFillService.cs create mode 100644 Minint.Core/Services/IFragmentService.cs create mode 100644 Minint.Core/Services/IGifExporter.cs create mode 100644 Minint.Core/Services/IImageEffectService.cs create mode 100644 Minint.Core/Services/IMinintSerializer.cs create mode 100644 Minint.Core/Services/IPaletteService.cs create mode 100644 Minint.Core/Services/IPatternGenerator.cs create mode 100644 Minint.Infrastructure/Minint.Infrastructure.csproj diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..985d571 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,482 @@ +# 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. 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. diff --git a/Minint.Core/Minint.Core.csproj b/Minint.Core/Minint.Core.csproj new file mode 100644 index 0000000..b760144 --- /dev/null +++ b/Minint.Core/Minint.Core.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/Minint.Core/Models/MinintContainer.cs b/Minint.Core/Models/MinintContainer.cs new file mode 100644 index 0000000..0a36ee9 --- /dev/null +++ b/Minint.Core/Models/MinintContainer.cs @@ -0,0 +1,35 @@ +namespace Minint.Core.Models; + +/// +/// Top-level container: holds dimensions shared by all documents/layers, +/// and a list of documents (frames). +/// +public sealed class MinintContainer +{ + public int Width { get; set; } + public int Height { get; set; } + public List Documents { get; } + + public int PixelCount => Width * Height; + + public MinintContainer(int width, int height) + { + ArgumentOutOfRangeException.ThrowIfLessThan(width, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(height, 1); + + Width = width; + Height = height; + Documents = []; + } + + /// + /// Creates a new document with a single transparent layer and adds it to the container. + /// + public MinintDocument AddNewDocument(string name) + { + var doc = new MinintDocument(name); + doc.Layers.Add(new MinintLayer("Layer 1", PixelCount)); + Documents.Add(doc); + return doc; + } +} diff --git a/Minint.Core/Models/MinintDocument.cs b/Minint.Core/Models/MinintDocument.cs new file mode 100644 index 0000000..9ec0b67 --- /dev/null +++ b/Minint.Core/Models/MinintDocument.cs @@ -0,0 +1,53 @@ +namespace Minint.Core.Models; + +/// +/// A single document (frame) within a container. +/// Has its own palette shared by all layers, plus a list of layers. +/// +public sealed class MinintDocument +{ + public string Name { get; set; } + + /// + /// Delay before showing the next frame during animation playback (ms). + /// + public uint FrameDelayMs { get; set; } + + /// + /// Document palette. Index 0 is always . + /// All layers reference colors by index into this list. + /// + public List Palette { get; } + + public List Layers { get; } + + public MinintDocument(string name) + { + Name = name; + FrameDelayMs = 100; + Palette = [RgbaColor.Transparent]; + Layers = []; + } + + /// + /// Constructor for deserialization — accepts pre-built palette and layers. + /// + public MinintDocument(string name, uint frameDelayMs, List palette, List layers) + { + Name = name; + FrameDelayMs = frameDelayMs; + Palette = palette; + Layers = layers; + } + + /// + /// Returns the number of bytes needed to store a single palette index on disk. + /// + public int IndexByteWidth => Palette.Count switch + { + <= 255 => 1, + <= 65_535 => 2, + <= 16_777_215 => 3, + _ => 4 + }; +} diff --git a/Minint.Core/Models/MinintLayer.cs b/Minint.Core/Models/MinintLayer.cs new file mode 100644 index 0000000..f767795 --- /dev/null +++ b/Minint.Core/Models/MinintLayer.cs @@ -0,0 +1,42 @@ +namespace Minint.Core.Models; + +/// +/// A single raster layer. Pixels are indices into the parent document's palette. +/// Array layout is row-major: Pixels[y * width + x]. +/// +public sealed class MinintLayer +{ + public string Name { get; set; } + public bool IsVisible { get; set; } + + /// + /// Per-layer opacity (0 = fully transparent, 255 = fully opaque). + /// Used during compositing: effective alpha = paletteColor.A * Opacity / 255. + /// + public byte Opacity { get; set; } + + /// + /// Palette indices, length must equal container Width * Height. + /// Index 0 = transparent by convention. + /// + public int[] Pixels { get; } + + public MinintLayer(string name, int pixelCount) + { + Name = name; + IsVisible = true; + Opacity = 255; + Pixels = new int[pixelCount]; + } + + /// + /// Constructor for deserialization — accepts a pre-filled pixel buffer. + /// + public MinintLayer(string name, bool isVisible, byte opacity, int[] pixels) + { + Name = name; + IsVisible = isVisible; + Opacity = opacity; + Pixels = pixels; + } +} diff --git a/Minint.Core/Models/RgbaColor.cs b/Minint.Core/Models/RgbaColor.cs new file mode 100644 index 0000000..11c001e --- /dev/null +++ b/Minint.Core/Models/RgbaColor.cs @@ -0,0 +1,37 @@ +using System.Runtime.InteropServices; + +namespace Minint.Core.Models; + +/// +/// 4-byte RGBA color value. Equality is component-wise. +/// +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public readonly record struct RgbaColor(byte R, byte G, byte B, byte A) +{ + public static readonly RgbaColor Transparent = new(0, 0, 0, 0); + public static readonly RgbaColor Black = new(0, 0, 0, 255); + public static readonly RgbaColor White = new(255, 255, 255, 255); + + /// + /// Packs color into a single uint as 0xAABBGGRR (little-endian RGBA). + /// Suitable for writing directly into BGRA bitmap buffers after byte-swap, + /// or for use as a dictionary key. + /// + public uint ToPackedRgba() => + (uint)(R | (G << 8) | (B << 16) | (A << 24)); + + public static RgbaColor FromPackedRgba(uint packed) => + new( + (byte)(packed & 0xFF), + (byte)((packed >> 8) & 0xFF), + (byte)((packed >> 16) & 0xFF), + (byte)((packed >> 24) & 0xFF)); + + /// + /// Packs as 0xAARRGGBB — used for Avalonia/SkiaSharp pixel buffers. + /// + public uint ToPackedArgb() => + (uint)(B | (G << 8) | (R << 16) | (A << 24)); + + public override string ToString() => $"#{R:X2}{G:X2}{B:X2}{A:X2}"; +} diff --git a/Minint.Core/Services/IBmpExporter.cs b/Minint.Core/Services/IBmpExporter.cs new file mode 100644 index 0000000..76dd064 --- /dev/null +++ b/Minint.Core/Services/IBmpExporter.cs @@ -0,0 +1,13 @@ +namespace Minint.Core.Services; + +public interface IBmpExporter +{ + /// + /// Exports a composited ARGB pixel buffer as a 32-bit BMP file. + /// + /// Output stream. + /// Pixel data packed as 0xAARRGGBB, row-major. + /// Image width. + /// Image height. + void Export(Stream stream, uint[] pixels, int width, int height); +} diff --git a/Minint.Core/Services/ICompositor.cs b/Minint.Core/Services/ICompositor.cs new file mode 100644 index 0000000..cdb9c00 --- /dev/null +++ b/Minint.Core/Services/ICompositor.cs @@ -0,0 +1,13 @@ +using Minint.Core.Models; + +namespace Minint.Core.Services; + +public interface ICompositor +{ + /// + /// Composites all visible layers of into a flat RGBA buffer. + /// Result is packed as ARGB (0xAARRGGBB) per pixel, row-major, length = width * height. + /// Layers are blended bottom-to-top with alpha compositing. + /// + uint[] Composite(MinintDocument document, int width, int height); +} diff --git a/Minint.Core/Services/IDrawingService.cs b/Minint.Core/Services/IDrawingService.cs new file mode 100644 index 0000000..971ae5f --- /dev/null +++ b/Minint.Core/Services/IDrawingService.cs @@ -0,0 +1,25 @@ +using Minint.Core.Models; + +namespace Minint.Core.Services; + +public interface IDrawingService +{ + /// + /// Applies a circular brush stroke at (, ) + /// with the given . Sets affected pixels to . + /// + void ApplyBrush(MinintLayer layer, int cx, int cy, int radius, int colorIndex, int width, int height); + + /// + /// Applies a circular eraser at (, ) + /// with the given . Sets affected pixels to index 0 (transparent). + /// + void ApplyEraser(MinintLayer layer, int cx, int cy, int radius, int width, int height); + + /// + /// Returns the set of pixel coordinates affected by a circular brush/eraser + /// centered at (, ) with given . + /// Used for tool preview overlay. + /// + List<(int X, int Y)> GetBrushMask(int cx, int cy, int radius, int width, int height); +} diff --git a/Minint.Core/Services/IFloodFillService.cs b/Minint.Core/Services/IFloodFillService.cs new file mode 100644 index 0000000..4d15965 --- /dev/null +++ b/Minint.Core/Services/IFloodFillService.cs @@ -0,0 +1,12 @@ +using Minint.Core.Models; + +namespace Minint.Core.Services; + +public interface IFloodFillService +{ + /// + /// Flood-fills a contiguous region of identical color starting at (, ) + /// with . Uses 4-connectivity (up/down/left/right). + /// + void Fill(MinintLayer layer, int x, int y, int newColorIndex, int width, int height); +} diff --git a/Minint.Core/Services/IFragmentService.cs b/Minint.Core/Services/IFragmentService.cs new file mode 100644 index 0000000..c3ddf98 --- /dev/null +++ b/Minint.Core/Services/IFragmentService.cs @@ -0,0 +1,29 @@ +using Minint.Core.Models; + +namespace Minint.Core.Services; + +public interface IFragmentService +{ + /// + /// Copies a rectangular region from one document/layer to another. + /// Palette colors are merged: missing colors are added to the destination palette. + /// + /// Source document. + /// Index of the source layer. + /// Source rectangle X origin. + /// Source rectangle Y origin. + /// Width of the region to copy. + /// Height of the region to copy. + /// Destination document. + /// Index of the destination layer. + /// Destination X origin. + /// Destination Y origin. + /// Container width (shared by both docs). + /// Container height (shared by both docs). + void CopyFragment( + MinintDocument srcDoc, int srcLayerIndex, + int srcX, int srcY, int regionWidth, int regionHeight, + MinintDocument dstDoc, int dstLayerIndex, + int dstX, int dstY, + int containerWidth, int containerHeight); +} diff --git a/Minint.Core/Services/IGifExporter.cs b/Minint.Core/Services/IGifExporter.cs new file mode 100644 index 0000000..56f1716 --- /dev/null +++ b/Minint.Core/Services/IGifExporter.cs @@ -0,0 +1,13 @@ +namespace Minint.Core.Services; + +public interface IGifExporter +{ + /// + /// Exports multiple frames as an animated GIF. + /// + /// Output stream. + /// Sequence of (ARGB pixels, delay in ms) per frame. + /// Frame width. + /// Frame height. + void Export(Stream stream, IReadOnlyList<(uint[] Pixels, uint DelayMs)> frames, int width, int height); +} diff --git a/Minint.Core/Services/IImageEffectService.cs b/Minint.Core/Services/IImageEffectService.cs new file mode 100644 index 0000000..c96c255 --- /dev/null +++ b/Minint.Core/Services/IImageEffectService.cs @@ -0,0 +1,20 @@ +using Minint.Core.Models; + +namespace Minint.Core.Services; + +public interface IImageEffectService +{ + /// + /// Adjusts contrast of the document by modifying palette colors. + /// > 1 increases contrast, < 1 decreases. + /// Index 0 (transparent) is not modified. + /// + void AdjustContrast(MinintDocument document, float factor); + + /// + /// Converts the document to grayscale by modifying palette colors. + /// Uses ITU-R BT.601 luminance: gray = 0.299R + 0.587G + 0.114B. + /// Index 0 (transparent) is not modified. + /// + void ToGrayscale(MinintDocument document); +} diff --git a/Minint.Core/Services/IMinintSerializer.cs b/Minint.Core/Services/IMinintSerializer.cs new file mode 100644 index 0000000..d7c1e87 --- /dev/null +++ b/Minint.Core/Services/IMinintSerializer.cs @@ -0,0 +1,17 @@ +using Minint.Core.Models; + +namespace Minint.Core.Services; + +public interface IMinintSerializer +{ + /// + /// Serializes the container to a binary .minint stream. + /// + void Write(Stream stream, MinintContainer container); + + /// + /// Deserializes a .minint stream into a container. + /// Throws on format/validation errors. + /// + MinintContainer Read(Stream stream); +} diff --git a/Minint.Core/Services/IPaletteService.cs b/Minint.Core/Services/IPaletteService.cs new file mode 100644 index 0000000..9abdfb6 --- /dev/null +++ b/Minint.Core/Services/IPaletteService.cs @@ -0,0 +1,23 @@ +using Minint.Core.Models; + +namespace Minint.Core.Services; + +public interface IPaletteService +{ + /// + /// Returns the index of in the document palette. + /// If the color is not present, appends it and returns the new index. + /// + int EnsureColor(MinintDocument document, RgbaColor color); + + /// + /// Finds index of an exact color match, or returns -1 if not found. + /// + int FindColor(MinintDocument document, RgbaColor color); + + /// + /// Removes unused colors from the palette and remaps all layer pixel indices. + /// Index 0 (transparent) is always preserved. + /// + void CompactPalette(MinintDocument document); +} diff --git a/Minint.Core/Services/IPatternGenerator.cs b/Minint.Core/Services/IPatternGenerator.cs new file mode 100644 index 0000000..e3db1df --- /dev/null +++ b/Minint.Core/Services/IPatternGenerator.cs @@ -0,0 +1,28 @@ +using Minint.Core.Models; + +namespace Minint.Core.Services; + +public enum PatternType +{ + Checkerboard, + HorizontalGradient, + VerticalGradient, + HorizontalStripes, + VerticalStripes, + ConcentricCircles, + Tile +} + +public interface IPatternGenerator +{ + /// + /// Generates a new document with a single layer filled with the specified pattern. + /// + /// Pattern type. + /// Image width in pixels. + /// Image height in pixels. + /// Colors to use (interpretation depends on pattern type). + /// Primary parameter: cell/stripe size, ring width, etc. + /// Secondary parameter (optional, pattern-dependent). + MinintDocument Generate(PatternType type, int width, int height, RgbaColor[] colors, int param1, int param2 = 0); +} diff --git a/Minint.Infrastructure/Minint.Infrastructure.csproj b/Minint.Infrastructure/Minint.Infrastructure.csproj new file mode 100644 index 0000000..4dc32fa --- /dev/null +++ b/Minint.Infrastructure/Minint.Infrastructure.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/Minint.slnx b/Minint.slnx index 358a6d7..af12a1b 100644 --- a/Minint.slnx +++ b/Minint.slnx @@ -1,3 +1,5 @@ + + diff --git a/Minint/Minint.csproj b/Minint/Minint.csproj index 66d57d3..137afa5 100644 --- a/Minint/Minint.csproj +++ b/Minint/Minint.csproj @@ -1,4 +1,4 @@ - + WinExe net10.0 @@ -9,16 +9,19 @@ - + + + + + - None All