735 lines
21 KiB
C#
735 lines
21 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.ComponentModel;
|
|
using System.Linq;
|
|
using Avalonia;
|
|
using Avalonia.Media.Imaging;
|
|
using Avalonia.Threading;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using Minint.Core.Models;
|
|
using Minint.Core.Services;
|
|
using Minint.Core.Services.Impl;
|
|
|
|
namespace Minint.ViewModels;
|
|
|
|
/// <summary>
|
|
/// Palette-independent clipboard fragment: stores resolved RGBA pixels.
|
|
/// </summary>
|
|
public sealed record ClipboardFragment(int Width, int Height, RgbaColor[] Pixels);
|
|
|
|
public partial class EditorViewModel : ViewModelBase
|
|
{
|
|
private readonly ICompositor _compositor = new Compositor();
|
|
private readonly IPaletteService _paletteService = new PaletteService();
|
|
private readonly IDrawingService _drawingService = new DrawingService();
|
|
private readonly IFloodFillService _floodFillService = new FloodFillService();
|
|
private readonly IFragmentService _fragmentService = new FragmentService();
|
|
|
|
[ObservableProperty]
|
|
[NotifyPropertyChangedFor(nameof(HasContainer))]
|
|
[NotifyPropertyChangedFor(nameof(Title))]
|
|
private MinintContainer? _container;
|
|
|
|
[ObservableProperty]
|
|
private MinintDocument? _activeDocument;
|
|
|
|
[ObservableProperty]
|
|
private MinintLayer? _activeLayer;
|
|
|
|
private bool _suppressDocumentSync;
|
|
|
|
[ObservableProperty]
|
|
private WriteableBitmap? _canvasBitmap;
|
|
|
|
[ObservableProperty]
|
|
private bool _showGrid;
|
|
|
|
// Tool state
|
|
[ObservableProperty]
|
|
private ToolType _activeTool = ToolType.Brush;
|
|
|
|
[ObservableProperty]
|
|
private int _brushRadius = 1;
|
|
|
|
[ObservableProperty]
|
|
private (int X, int Y)? _previewCenter;
|
|
|
|
private Avalonia.Media.Color _previewColor = Avalonia.Media.Color.FromArgb(255, 0, 0, 0);
|
|
|
|
public Avalonia.Media.Color PreviewColor
|
|
{
|
|
get => _previewColor;
|
|
set
|
|
{
|
|
if (_previewColor == value) return;
|
|
_previewColor = value;
|
|
OnPropertyChanged();
|
|
OnPropertyChanged(nameof(SelectedColor));
|
|
}
|
|
}
|
|
|
|
public RgbaColor SelectedColor => new(_previewColor.R, _previewColor.G, _previewColor.B, _previewColor.A);
|
|
|
|
// Selection state (Select tool rubber-band)
|
|
[ObservableProperty]
|
|
[NotifyPropertyChangedFor(nameof(HasSelection))]
|
|
private (int X, int Y, int W, int H)? _selectionRect;
|
|
|
|
public bool HasSelection => SelectionRect is not null;
|
|
|
|
// Clipboard
|
|
[ObservableProperty]
|
|
[NotifyPropertyChangedFor(nameof(HasClipboard))]
|
|
private ClipboardFragment? _clipboard;
|
|
|
|
public bool HasClipboard => Clipboard is not null;
|
|
|
|
// Paste mode
|
|
[ObservableProperty]
|
|
[NotifyPropertyChangedFor(nameof(IsPasting))]
|
|
private (int X, int Y)? _pastePosition;
|
|
|
|
public bool IsPasting => PastePosition is not null;
|
|
|
|
[ObservableProperty]
|
|
[NotifyPropertyChangedFor(nameof(Title))]
|
|
private string? _filePath;
|
|
|
|
public bool HasContainer => Container is not null;
|
|
|
|
public string Title => FilePath is not null
|
|
? $"Minint — {System.IO.Path.GetFileName(FilePath)}"
|
|
: Container is not null
|
|
? "Minint — Untitled"
|
|
: "Minint";
|
|
|
|
public ObservableCollection<MinintDocument> Documents { get; } = [];
|
|
public ObservableCollection<MinintLayer> Layers { get; } = [];
|
|
|
|
#region Container / Document management
|
|
|
|
public void NewContainer(int width, int height)
|
|
{
|
|
var c = new MinintContainer(width, height);
|
|
c.AddNewDocument("Document 1");
|
|
LoadContainer(c, null);
|
|
}
|
|
|
|
public void LoadContainer(MinintContainer container, string? path)
|
|
{
|
|
Container = container;
|
|
FilePath = path;
|
|
|
|
SyncDocumentsList();
|
|
SelectDocument(container.Documents.Count > 0 ? container.Documents[0] : null);
|
|
}
|
|
|
|
partial void OnActiveDocumentChanged(MinintDocument? value)
|
|
{
|
|
if (_suppressDocumentSync) return;
|
|
SyncLayersAndCanvas(value);
|
|
}
|
|
|
|
public void SelectDocument(MinintDocument? doc)
|
|
{
|
|
_suppressDocumentSync = true;
|
|
ActiveDocument = doc;
|
|
_suppressDocumentSync = false;
|
|
SyncLayersAndCanvas(doc);
|
|
}
|
|
|
|
public void SyncAfterExternalChange() => SyncDocumentsList();
|
|
|
|
private void SyncDocumentsList()
|
|
{
|
|
Documents.Clear();
|
|
if (Container is null) return;
|
|
foreach (var doc in Container.Documents)
|
|
Documents.Add(doc);
|
|
}
|
|
|
|
private void SyncLayersAndCanvas(MinintDocument? doc)
|
|
{
|
|
UnsubscribeLayerVisibility();
|
|
Layers.Clear();
|
|
if (doc is not null)
|
|
{
|
|
foreach (var layer in doc.Layers)
|
|
Layers.Add(layer);
|
|
ActiveLayer = doc.Layers.Count > 0 ? doc.Layers[0] : null;
|
|
}
|
|
else
|
|
{
|
|
ActiveLayer = null;
|
|
}
|
|
SubscribeLayerVisibility();
|
|
RefreshCanvas();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Layer visibility change tracking
|
|
|
|
private void SubscribeLayerVisibility()
|
|
{
|
|
foreach (var layer in Layers)
|
|
{
|
|
if (layer is INotifyPropertyChanged npc)
|
|
npc.PropertyChanged += OnLayerPropertyChanged;
|
|
}
|
|
}
|
|
|
|
private void UnsubscribeLayerVisibility()
|
|
{
|
|
foreach (var layer in Layers)
|
|
{
|
|
if (layer is INotifyPropertyChanged npc)
|
|
npc.PropertyChanged -= OnLayerPropertyChanged;
|
|
}
|
|
}
|
|
|
|
private void OnLayerPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
{
|
|
if (e.PropertyName is nameof(MinintLayer.IsVisible) or nameof(MinintLayer.Opacity))
|
|
RefreshCanvas();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Document commands
|
|
|
|
[RelayCommand]
|
|
private void AddDocument()
|
|
{
|
|
if (Container is null) return;
|
|
int num = Container.Documents.Count + 1;
|
|
var doc = Container.AddNewDocument($"Document {num}");
|
|
Documents.Add(doc);
|
|
SelectDocument(doc);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void RemoveDocument()
|
|
{
|
|
if (Container is null || ActiveDocument is null) return;
|
|
if (Container.Documents.Count <= 1) return;
|
|
|
|
var doc = ActiveDocument;
|
|
int idx = Container.Documents.IndexOf(doc);
|
|
Container.Documents.Remove(doc);
|
|
Documents.Remove(doc);
|
|
|
|
int newIdx = Math.Min(idx, Container.Documents.Count - 1);
|
|
SelectDocument(newIdx >= 0 ? Container.Documents[newIdx] : null);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void RenameDocument() { }
|
|
|
|
[RelayCommand]
|
|
private void MoveDocumentUp()
|
|
{
|
|
if (Container is null || ActiveDocument is null) return;
|
|
int idx = Container.Documents.IndexOf(ActiveDocument);
|
|
if (idx <= 0) return;
|
|
(Container.Documents[idx], Container.Documents[idx - 1]) = (Container.Documents[idx - 1], Container.Documents[idx]);
|
|
SyncDocumentsList();
|
|
_suppressDocumentSync = true;
|
|
ActiveDocument = Container.Documents[idx - 1];
|
|
_suppressDocumentSync = false;
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void MoveDocumentDown()
|
|
{
|
|
if (Container is null || ActiveDocument is null) return;
|
|
int idx = Container.Documents.IndexOf(ActiveDocument);
|
|
if (idx < 0 || idx >= Container.Documents.Count - 1) return;
|
|
(Container.Documents[idx], Container.Documents[idx + 1]) = (Container.Documents[idx + 1], Container.Documents[idx]);
|
|
SyncDocumentsList();
|
|
_suppressDocumentSync = true;
|
|
ActiveDocument = Container.Documents[idx + 1];
|
|
_suppressDocumentSync = false;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Layer commands
|
|
|
|
[RelayCommand]
|
|
private void AddLayer()
|
|
{
|
|
if (Container is null || ActiveDocument is null) return;
|
|
int num = ActiveDocument.Layers.Count + 1;
|
|
var layer = new MinintLayer($"Layer {num}", Container.PixelCount);
|
|
ActiveDocument.Layers.Add(layer);
|
|
Layers.Add(layer);
|
|
ActiveLayer = layer;
|
|
SubscribeLayerVisibility();
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void RemoveLayer()
|
|
{
|
|
if (ActiveDocument is null || ActiveLayer is null) return;
|
|
if (ActiveDocument.Layers.Count <= 1) return;
|
|
|
|
UnsubscribeLayerVisibility();
|
|
var layer = ActiveLayer;
|
|
int idx = ActiveDocument.Layers.IndexOf(layer);
|
|
ActiveDocument.Layers.Remove(layer);
|
|
Layers.Remove(layer);
|
|
|
|
int newIdx = Math.Min(idx, ActiveDocument.Layers.Count - 1);
|
|
ActiveLayer = newIdx >= 0 ? ActiveDocument.Layers[newIdx] : null;
|
|
SubscribeLayerVisibility();
|
|
RefreshCanvas();
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void MoveLayerUp()
|
|
{
|
|
if (ActiveDocument is null || ActiveLayer is null) return;
|
|
int idx = ActiveDocument.Layers.IndexOf(ActiveLayer);
|
|
if (idx <= 0) return;
|
|
|
|
UnsubscribeLayerVisibility();
|
|
var layer = ActiveLayer;
|
|
(ActiveDocument.Layers[idx], ActiveDocument.Layers[idx - 1]) = (ActiveDocument.Layers[idx - 1], ActiveDocument.Layers[idx]);
|
|
Layers.Move(idx, idx - 1);
|
|
ActiveLayer = layer;
|
|
SubscribeLayerVisibility();
|
|
RefreshCanvas();
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void MoveLayerDown()
|
|
{
|
|
if (ActiveDocument is null || ActiveLayer is null) return;
|
|
int idx = ActiveDocument.Layers.IndexOf(ActiveLayer);
|
|
if (idx < 0 || idx >= ActiveDocument.Layers.Count - 1) return;
|
|
|
|
UnsubscribeLayerVisibility();
|
|
var layer = ActiveLayer;
|
|
(ActiveDocument.Layers[idx], ActiveDocument.Layers[idx + 1]) = (ActiveDocument.Layers[idx + 1], ActiveDocument.Layers[idx]);
|
|
Layers.Move(idx, idx + 1);
|
|
ActiveLayer = layer;
|
|
SubscribeLayerVisibility();
|
|
RefreshCanvas();
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void DuplicateLayer()
|
|
{
|
|
if (Container is null || ActiveDocument is null || ActiveLayer is null) return;
|
|
var src = ActiveLayer;
|
|
var dup = new MinintLayer(src.Name + " copy", src.IsVisible, src.Opacity, (int[])src.Pixels.Clone());
|
|
int idx = ActiveDocument.Layers.IndexOf(src) + 1;
|
|
ActiveDocument.Layers.Insert(idx, dup);
|
|
|
|
UnsubscribeLayerVisibility();
|
|
Layers.Insert(idx, dup);
|
|
ActiveLayer = dup;
|
|
SubscribeLayerVisibility();
|
|
RefreshCanvas();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Drawing
|
|
|
|
public void OnToolDown(int px, int py)
|
|
{
|
|
if (IsPlaying) return;
|
|
if (Container is null || ActiveDocument is null || ActiveLayer is null)
|
|
return;
|
|
|
|
int w = Container.Width, h = Container.Height;
|
|
if (px < 0 || px >= w || py < 0 || py >= h)
|
|
return;
|
|
|
|
if (ActiveTool == ToolType.Select) return; // handled separately
|
|
|
|
switch (ActiveTool)
|
|
{
|
|
case ToolType.Brush:
|
|
{
|
|
int colorIdx = ActiveDocument.EnsureColorCached(SelectedColor);
|
|
_drawingService.ApplyBrush(ActiveLayer, px, py, BrushRadius, colorIdx, w, h);
|
|
break;
|
|
}
|
|
case ToolType.Eraser:
|
|
_drawingService.ApplyEraser(ActiveLayer, px, py, BrushRadius, w, h);
|
|
break;
|
|
case ToolType.Fill:
|
|
{
|
|
int colorIdx = ActiveDocument.EnsureColorCached(SelectedColor);
|
|
_floodFillService.Fill(ActiveLayer, px, py, colorIdx, w, h);
|
|
break;
|
|
}
|
|
}
|
|
|
|
RefreshCanvas();
|
|
}
|
|
|
|
public void OnToolDrag(int px, int py)
|
|
{
|
|
if (IsPlaying) return;
|
|
if (ActiveTool is ToolType.Fill or ToolType.Select) return;
|
|
OnToolDown(px, py);
|
|
}
|
|
|
|
public List<(int X, int Y)>? GetPreviewMask()
|
|
{
|
|
if (PreviewCenter is null || Container is null)
|
|
return null;
|
|
if (ActiveTool is ToolType.Fill or ToolType.Select)
|
|
return null;
|
|
|
|
var (cx, cy) = PreviewCenter.Value;
|
|
return _drawingService.GetBrushMask(cx, cy, BrushRadius, Container.Width, Container.Height);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void SelectBrush() { CancelPasteMode(); ActiveTool = ToolType.Brush; }
|
|
|
|
[RelayCommand]
|
|
private void SelectEraser() { CancelPasteMode(); ActiveTool = ToolType.Eraser; }
|
|
|
|
[RelayCommand]
|
|
private void SelectFill() { CancelPasteMode(); ActiveTool = ToolType.Fill; }
|
|
|
|
[RelayCommand]
|
|
private void SelectSelectTool() { CancelPasteMode(); ActiveTool = ToolType.Select; }
|
|
|
|
#endregion
|
|
|
|
#region Selection + Copy/Paste (A4)
|
|
|
|
/// <summary>Called by PixelCanvas when selection drag starts.</summary>
|
|
public void BeginSelection(int px, int py)
|
|
{
|
|
if (IsPlaying) return;
|
|
SelectionRect = (px, py, 0, 0);
|
|
}
|
|
|
|
/// <summary>Called by PixelCanvas as the user drags.</summary>
|
|
public void UpdateSelection(int px, int py)
|
|
{
|
|
if (SelectionRect is null) return;
|
|
var s = SelectionRect.Value;
|
|
int x = Math.Min(s.X, px);
|
|
int y = Math.Min(s.Y, py);
|
|
int w = Math.Abs(px - s.X) + 1;
|
|
int h = Math.Abs(py - s.Y) + 1;
|
|
// Store normalized rect but keep original anchor in _selAnchor
|
|
_selectionRectNormalized = (x, y, w, h);
|
|
}
|
|
|
|
/// <summary>Called by PixelCanvas when mouse is released.</summary>
|
|
public void FinishSelection(int px, int py)
|
|
{
|
|
if (SelectionRect is null) return;
|
|
var s = SelectionRect.Value;
|
|
int x0 = Math.Min(s.X, px);
|
|
int y0 = Math.Min(s.Y, py);
|
|
int rw = Math.Abs(px - s.X) + 1;
|
|
int rh = Math.Abs(py - s.Y) + 1;
|
|
if (Container is not null)
|
|
{
|
|
x0 = Math.Max(0, x0);
|
|
y0 = Math.Max(0, y0);
|
|
rw = Math.Min(rw, Container.Width - x0);
|
|
rh = Math.Min(rh, Container.Height - y0);
|
|
}
|
|
if (rw <= 0 || rh <= 0)
|
|
{
|
|
SelectionRect = null;
|
|
_selectionRectNormalized = null;
|
|
return;
|
|
}
|
|
SelectionRect = (x0, y0, rw, rh);
|
|
_selectionRectNormalized = SelectionRect;
|
|
}
|
|
|
|
private (int X, int Y, int W, int H)? _selectionRectNormalized;
|
|
|
|
/// <summary>The normalized (positive W/H, clamped) selection rectangle for rendering.</summary>
|
|
public (int X, int Y, int W, int H)? SelectionRectNormalized => _selectionRectNormalized;
|
|
|
|
[RelayCommand]
|
|
private void CopySelection()
|
|
{
|
|
if (SelectionRect is null || ActiveDocument is null || ActiveLayer is null || Container is null)
|
|
return;
|
|
|
|
var (sx, sy, sw, sh) = SelectionRect.Value;
|
|
int cw = Container.Width;
|
|
var palette = ActiveDocument.Palette;
|
|
var srcPixels = ActiveLayer.Pixels;
|
|
var buf = new RgbaColor[sw * sh];
|
|
|
|
for (int dy = 0; dy < sh; dy++)
|
|
{
|
|
int srcRow = sy + dy;
|
|
for (int dx = 0; dx < sw; dx++)
|
|
{
|
|
int srcCol = sx + dx;
|
|
int idx = srcPixels[srcRow * cw + srcCol];
|
|
buf[dy * sw + dx] = idx < palette.Count ? palette[idx] : RgbaColor.Transparent;
|
|
}
|
|
}
|
|
|
|
Clipboard = new ClipboardFragment(sw, sh, buf);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void PasteClipboard()
|
|
{
|
|
if (Clipboard is null) return;
|
|
PastePosition = (0, 0);
|
|
}
|
|
|
|
public void MovePaste(int px, int py)
|
|
{
|
|
if (IsPlaying || !IsPasting) return;
|
|
PastePosition = (px, py);
|
|
}
|
|
|
|
[RelayCommand]
|
|
public void CommitPaste()
|
|
{
|
|
if (IsPlaying) return;
|
|
if (!IsPasting || Clipboard is null || ActiveDocument is null || ActiveLayer is null || Container is null)
|
|
return;
|
|
|
|
var (px, py) = PastePosition!.Value;
|
|
int cw = Container.Width, ch = Container.Height;
|
|
var frag = Clipboard;
|
|
var dstPixels = ActiveLayer.Pixels;
|
|
|
|
for (int fy = 0; fy < frag.Height; fy++)
|
|
{
|
|
int dy = py + fy;
|
|
if (dy < 0 || dy >= ch) continue;
|
|
for (int fx = 0; fx < frag.Width; fx++)
|
|
{
|
|
int dx = px + fx;
|
|
if (dx < 0 || dx >= cw) continue;
|
|
var color = frag.Pixels[fy * frag.Width + fx];
|
|
if (color.A == 0) continue; // skip transparent
|
|
int colorIdx = ActiveDocument.EnsureColorCached(color);
|
|
dstPixels[dy * cw + dx] = colorIdx;
|
|
}
|
|
}
|
|
|
|
PastePosition = null;
|
|
SelectionRect = null;
|
|
_selectionRectNormalized = null;
|
|
RefreshCanvas();
|
|
}
|
|
|
|
[RelayCommand]
|
|
public void CancelPaste()
|
|
{
|
|
PastePosition = null;
|
|
}
|
|
|
|
public void ClearSelection()
|
|
{
|
|
SelectionRect = null;
|
|
_selectionRectNormalized = null;
|
|
}
|
|
|
|
private void CancelPasteMode()
|
|
{
|
|
PastePosition = null;
|
|
SelectionRect = null;
|
|
_selectionRectNormalized = null;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Canvas rendering
|
|
|
|
public void RefreshCanvas()
|
|
{
|
|
if (Container is null || ActiveDocument is null)
|
|
{
|
|
CanvasBitmap = null;
|
|
return;
|
|
}
|
|
|
|
int w = Container.Width;
|
|
int h = Container.Height;
|
|
uint[] argb = _compositor.Composite(ActiveDocument, w, h);
|
|
|
|
var bmp = new WriteableBitmap(
|
|
new PixelSize(w, h),
|
|
new Vector(96, 96),
|
|
Avalonia.Platform.PixelFormat.Bgra8888);
|
|
|
|
using (var fb = bmp.Lock())
|
|
{
|
|
unsafe
|
|
{
|
|
var dst = new Span<uint>((void*)fb.Address, w * h);
|
|
for (int i = 0; i < argb.Length; i++)
|
|
{
|
|
uint px = argb[i];
|
|
byte a = (byte)(px >> 24);
|
|
byte r = (byte)((px >> 16) & 0xFF);
|
|
byte g = (byte)((px >> 8) & 0xFF);
|
|
byte b = (byte)(px & 0xFF);
|
|
|
|
if (a == 255)
|
|
{
|
|
dst[i] = px;
|
|
}
|
|
else if (a == 0)
|
|
{
|
|
dst[i] = 0;
|
|
}
|
|
else
|
|
{
|
|
r = (byte)(r * a / 255);
|
|
g = (byte)(g * a / 255);
|
|
b = (byte)(b * a / 255);
|
|
dst[i] = (uint)(b | (g << 8) | (r << 16) | (a << 24));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
CanvasBitmap = bmp;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Animation playback
|
|
|
|
private DispatcherTimer? _animationTimer;
|
|
private int _animationFrameIndex;
|
|
|
|
[ObservableProperty]
|
|
[NotifyPropertyChangedFor(nameof(IsNotPlaying))]
|
|
private bool _isPlaying;
|
|
|
|
public bool IsNotPlaying => !IsPlaying;
|
|
|
|
[RelayCommand]
|
|
private void PlayAnimation()
|
|
{
|
|
if (Container is null || Container.Documents.Count < 2) return;
|
|
if (IsPlaying) return;
|
|
|
|
IsPlaying = true;
|
|
_animationFrameIndex = ActiveDocument is not null
|
|
? Container.Documents.IndexOf(ActiveDocument)
|
|
: 0;
|
|
if (_animationFrameIndex < 0) _animationFrameIndex = 0;
|
|
|
|
AdvanceAnimationFrame();
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void StopAnimation()
|
|
{
|
|
_animationTimer?.Stop();
|
|
_animationTimer = null;
|
|
IsPlaying = false;
|
|
}
|
|
|
|
private void AdvanceAnimationFrame()
|
|
{
|
|
if (Container is null || !IsPlaying)
|
|
{
|
|
StopAnimation();
|
|
return;
|
|
}
|
|
|
|
var docs = Container.Documents;
|
|
if (docs.Count == 0)
|
|
{
|
|
StopAnimation();
|
|
return;
|
|
}
|
|
|
|
_animationFrameIndex %= docs.Count;
|
|
var doc = docs[_animationFrameIndex];
|
|
|
|
_suppressDocumentSync = true;
|
|
ActiveDocument = doc;
|
|
_suppressDocumentSync = false;
|
|
RefreshCanvasFor(doc);
|
|
|
|
uint delay = doc.FrameDelayMs;
|
|
if (delay < 10) delay = 10;
|
|
|
|
_animationTimer?.Stop();
|
|
_animationTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(delay) };
|
|
_animationTimer.Tick += (_, _) =>
|
|
{
|
|
_animationTimer?.Stop();
|
|
_animationFrameIndex++;
|
|
AdvanceAnimationFrame();
|
|
};
|
|
_animationTimer.Start();
|
|
}
|
|
|
|
private void RefreshCanvasFor(MinintDocument doc)
|
|
{
|
|
if (Container is null)
|
|
{
|
|
CanvasBitmap = null;
|
|
return;
|
|
}
|
|
|
|
int w = Container.Width;
|
|
int h = Container.Height;
|
|
uint[] argb = _compositor.Composite(doc, w, h);
|
|
|
|
var bmp = new WriteableBitmap(
|
|
new PixelSize(w, h),
|
|
new Vector(96, 96),
|
|
Avalonia.Platform.PixelFormat.Bgra8888);
|
|
|
|
using (var fb = bmp.Lock())
|
|
{
|
|
unsafe
|
|
{
|
|
var dst = new Span<uint>((void*)fb.Address, w * h);
|
|
for (int i = 0; i < argb.Length; i++)
|
|
{
|
|
uint px = argb[i];
|
|
byte a2 = (byte)(px >> 24);
|
|
byte r2 = (byte)((px >> 16) & 0xFF);
|
|
byte g2 = (byte)((px >> 8) & 0xFF);
|
|
byte b2 = (byte)(px & 0xFF);
|
|
|
|
if (a2 == 255) { dst[i] = px; }
|
|
else if (a2 == 0) { dst[i] = 0; }
|
|
else
|
|
{
|
|
r2 = (byte)(r2 * a2 / 255);
|
|
g2 = (byte)(g2 * a2 / 255);
|
|
b2 = (byte)(b2 * a2 / 255);
|
|
dst[i] = (uint)(b2 | (g2 << 8) | (r2 << 16) | (a2 << 24));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
CanvasBitmap = bmp;
|
|
}
|
|
|
|
#endregion
|
|
|
|
public ICompositor Compositor => _compositor;
|
|
public IPaletteService PaletteService => _paletteService;
|
|
public IDrawingService DrawingService => _drawingService;
|
|
public IFragmentService FragmentService => _fragmentService;
|
|
}
|