Skip to content

Commit d0d86a5

Browse files
feat(merge): animated sprite previews in merge and batch transplant dialogs
1 parent 43f7c06 commit d0d86a5

1 file changed

Lines changed: 89 additions & 7 deletions

File tree

src/App/ViewModels/MainWindowViewModel.cs

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -995,6 +995,10 @@ is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetim
995995

996996
// Build item rows with sprite previews rendered from source SPR
997997
var itemsPanel = new Avalonia.Controls.StackPanel { Spacing = 2 };
998+
999+
// Track animated entries for the dialog's animation timer
1000+
var animatedEntries = new List<(Avalonia.Controls.Image img, DatThingType thing, int frames, int currentFrame)>();
1001+
9981002
foreach (var entry in entries)
9991003
{
10001004
var rowGrid = new Avalonia.Controls.Grid
@@ -1032,15 +1036,21 @@ is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetim
10321036
ClipToBounds = true,
10331037
};
10341038
var bmp = ComposeThingBitmapStatic(entry.SourceThing, sourceSpr);
1039+
Avalonia.Controls.Image? spriteImg = null;
10351040
if (bmp != null)
10361041
{
1037-
var img = new Avalonia.Controls.Image
1042+
spriteImg = new Avalonia.Controls.Image
10381043
{
10391044
Source = bmp, Width = 32, Height = 32,
10401045
Stretch = Avalonia.Media.Stretch.Uniform,
10411046
};
1042-
Avalonia.Media.RenderOptions.SetBitmapInterpolationMode(img, Avalonia.Media.Imaging.BitmapInterpolationMode.None);
1043-
spriteBorder.Child = img;
1047+
Avalonia.Media.RenderOptions.SetBitmapInterpolationMode(spriteImg, Avalonia.Media.Imaging.BitmapInterpolationMode.None);
1048+
spriteBorder.Child = spriteImg;
1049+
1050+
// Register for animation if thing has multiple frames
1051+
var fg0 = entry.SourceThing.FrameGroups.Length > 0 ? entry.SourceThing.FrameGroups[0] : null;
1052+
if (fg0 != null && fg0.Frames > 1)
1053+
animatedEntries.Add((spriteImg, entry.SourceThing, fg0.Frames, 0));
10441054
}
10451055
Avalonia.Controls.Grid.SetColumn(spriteBorder, 1);
10461056
rowGrid.Children.Add(spriteBorder);
@@ -1189,6 +1199,37 @@ is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetim
11891199
buttonPanel.Children.Add(cancelBtn);
11901200
buttonPanel.Children.Add(confirmBtn);
11911201

1202+
// Animation timer for sprite previews in the merge dialog
1203+
DispatcherTimer? mergeAnimTimer = null;
1204+
if (animatedEntries.Count > 0)
1205+
{
1206+
var animState = animatedEntries.Select(e => new { e.img, e.thing, e.frames, frame = new int[] { 0 } }).ToList();
1207+
int tickCounter = 0;
1208+
mergeAnimTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) };
1209+
mergeAnimTimer.Tick += (_, _) =>
1210+
{
1211+
tickCounter++;
1212+
foreach (var s in animState)
1213+
{
1214+
int divisor = s.thing.Category switch
1215+
{
1216+
ThingCategory.Effect => 1,
1217+
ThingCategory.Missile => 1,
1218+
ThingCategory.Outfit => 3,
1219+
_ => 5,
1220+
};
1221+
if (tickCounter % divisor != 0) continue;
1222+
s.frame[0] = (s.frame[0] + 1) % s.frames;
1223+
var newBmp = ComposeThingBitmapStatic(s.thing, sourceSpr, s.frame[0]);
1224+
if (newBmp != null)
1225+
s.img.Source = newBmp;
1226+
}
1227+
};
1228+
mergeAnimTimer.Start();
1229+
}
1230+
1231+
dialog.Closed += (_, _) => mergeAnimTimer?.Stop();
1232+
11921233
await dialog.ShowDialog(window);
11931234
}
11941235

@@ -1251,6 +1292,10 @@ is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetim
12511292

12521293
// Build the item list with sprite previews
12531294
var itemsPanel = new Avalonia.Controls.StackPanel { Spacing = 2 };
1295+
1296+
// Track animated entries for the dialog's animation timer
1297+
var batchAnimEntries = new List<(Avalonia.Controls.Image img, DatThingType thing, int frames, int currentFrame)>();
1298+
12541299
foreach (var entry in entries)
12551300
{
12561301
var rowGrid = new Avalonia.Controls.Grid
@@ -1286,7 +1331,7 @@ is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetim
12861331
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
12871332
ClipToBounds = true,
12881333
};
1289-
var bmp = ComposeThingBitmap(entry.SourceThing);
1334+
var bmp = ComposeThingBitmapStatic(entry.SourceThing, sourceSpr);
12901335
if (bmp != null)
12911336
{
12921337
var img = new Avalonia.Controls.Image
@@ -1296,6 +1341,10 @@ is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetim
12961341
};
12971342
Avalonia.Media.RenderOptions.SetBitmapInterpolationMode(img, Avalonia.Media.Imaging.BitmapInterpolationMode.None);
12981343
spriteBorder.Child = img;
1344+
1345+
var fg0 = entry.SourceThing.FrameGroups.Length > 0 ? entry.SourceThing.FrameGroups[0] : null;
1346+
if (fg0 != null && fg0.Frames > 1)
1347+
batchAnimEntries.Add((img, entry.SourceThing, fg0.Frames, 0));
12991348
}
13001349
Avalonia.Controls.Grid.SetColumn(spriteBorder, 1);
13011350
rowGrid.Children.Add(spriteBorder);
@@ -1457,6 +1506,37 @@ is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetim
14571506
buttonPanel.Children.Add(cancelBtn);
14581507
buttonPanel.Children.Add(confirmBtn);
14591508

1509+
// Animation timer for sprite previews in the batch transplant dialog
1510+
DispatcherTimer? batchAnimTimer = null;
1511+
if (batchAnimEntries.Count > 0)
1512+
{
1513+
var animState = batchAnimEntries.Select(e => new { e.img, e.thing, e.frames, frame = new int[] { 0 } }).ToList();
1514+
int tickCounter = 0;
1515+
batchAnimTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) };
1516+
batchAnimTimer.Tick += (_, _) =>
1517+
{
1518+
tickCounter++;
1519+
foreach (var s in animState)
1520+
{
1521+
int divisor = s.thing.Category switch
1522+
{
1523+
ThingCategory.Effect => 1,
1524+
ThingCategory.Missile => 1,
1525+
ThingCategory.Outfit => 3,
1526+
_ => 5,
1527+
};
1528+
if (tickCounter % divisor != 0) continue;
1529+
s.frame[0] = (s.frame[0] + 1) % s.frames;
1530+
var newBmp = ComposeThingBitmapStatic(s.thing, sourceSpr, s.frame[0]);
1531+
if (newBmp != null)
1532+
s.img.Source = newBmp;
1533+
}
1534+
};
1535+
batchAnimTimer.Start();
1536+
}
1537+
1538+
dialog.Closed += (_, _) => batchAnimTimer?.Stop();
1539+
14601540
await dialog.ShowDialog(window);
14611541
}
14621542

@@ -6293,7 +6373,7 @@ private void RefreshAfterSpriteEdit(uint spriteId)
62936373
/// Static version of ComposeThingBitmap that takes an explicit SprFile.
62946374
/// Used for composing sprites in a target session context (e.g. after transplant).
62956375
/// </summary>
6296-
internal static WriteableBitmap? ComposeThingBitmapStatic(DatThingType thing, SprFile sprFile)
6376+
internal static WriteableBitmap? ComposeThingBitmapStatic(DatThingType thing, SprFile sprFile, int frame = 0)
62976377
{
62986378
if (thing.FrameGroups.Length == 0) return null;
62996379

@@ -6302,13 +6382,15 @@ private void RefreshAfterSpriteEdit(uint spriteId)
63026382
int h = fg.Height;
63036383
if (w == 0 || h == 0) return null;
63046384

6385+
int clampedFrame = Math.Clamp(frame, 0, Math.Max(0, fg.Frames - 1));
6386+
63056387
// Single 1×1 item
63066388
if (w == 1 && h == 1 && fg.Layers == 1)
63076389
{
63086390
int px = 0;
63096391
if (thing.Category == ThingCategory.Outfit && fg.PatternX > 2)
63106392
px = 2;
6311-
uint sprId = fg.GetSpriteId(0, 0, 0, px, 0, 0, 0);
6393+
uint sprId = fg.GetSpriteId(0, 0, 0, px, 0, 0, clampedFrame);
63126394
var rgba = sprFile.GetSpriteRgba(sprId);
63136395
if (rgba == null) return null;
63146396
try
@@ -6340,7 +6422,7 @@ private void RefreshAfterSpriteEdit(uint spriteId)
63406422
{
63416423
for (int th = 0; th < h; th++)
63426424
{
6343-
uint sprId = fg.GetSpriteId(tw, th, l, patX, 0, 0, 0);
6425+
uint sprId = fg.GetSpriteId(tw, th, l, patX, 0, 0, clampedFrame);
63446426
var rgba = sprFile.GetSpriteRgba(sprId);
63456427
if (rgba == null) continue;
63466428

0 commit comments

Comments
 (0)