@ai-game-assets/phaser 0.5.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -126,7 +126,12 @@ export function createDesignerElements(options, manifest, selectedAssetId) {
126
126
  currentTouchUpButton.className = "ai-game-assets-designer__animate-button";
127
127
  currentTouchUpButton.textContent = "Touch up...";
128
128
  currentTouchUpButton.hidden = true;
129
- currentPreview.append(currentImage, currentAudio, currentAnimation, currentAnimationButton, currentTouchUpButton);
129
+ const currentRevertButton = document.createElement("button");
130
+ currentRevertButton.type = "button";
131
+ currentRevertButton.className = "ai-game-assets-designer__animate-button";
132
+ currentRevertButton.textContent = "Revert preview";
133
+ currentRevertButton.hidden = true;
134
+ currentPreview.append(currentImage, currentAudio, currentAnimation, currentAnimationButton, currentTouchUpButton, currentRevertButton);
130
135
  const regenerateButton = document.createElement("button");
131
136
  regenerateButton.type = "button";
132
137
  regenerateButton.textContent = "Regenerate";
@@ -161,6 +166,7 @@ export function createDesignerElements(options, manifest, selectedAssetId) {
161
166
  root,
162
167
  toggle,
163
168
  panel,
169
+ title,
164
170
  styleButton,
165
171
  assetSelect,
166
172
  assetBrowser,
@@ -189,6 +195,7 @@ export function createDesignerElements(options, manifest, selectedAssetId) {
189
195
  currentAudio,
190
196
  currentAnimation,
191
197
  currentAnimationButton,
198
+ currentRevertButton,
192
199
  currentTouchUpButton,
193
200
  currentPreview,
194
201
  uploadButton,
@@ -214,7 +221,7 @@ export function renderAssetFolderBrowser(options) {
214
221
  const storedPathValue = options.container.getAttribute("data-path");
215
222
  const storedPath = storedPathValue !== null
216
223
  ? storedPathValue.split("/").filter(Boolean)
217
- : selectedPath;
224
+ : [];
218
225
  const currentPath = pathHasAnyAsset(storedPath, assetPathEntries)
219
226
  ? storedPath
220
227
  : selectedPath;
@@ -390,7 +397,7 @@ export function renderOptions(options) {
390
397
  element: animationStage,
391
398
  src: option.dataUrl,
392
399
  asset: optionAsset,
393
- displaySize: resolvePreviewDisplaySize(options.designerOptions, options.assetId, optionAsset),
400
+ displaySize: { width: 68, height: 68 },
394
401
  applyFrameTransforms: false
395
402
  });
396
403
  });
@@ -473,7 +480,6 @@ export async function uploadedOptionFromFile(options) {
473
480
  model: "uploaded",
474
481
  dimensions: geometry.dimensions,
475
482
  frameGrid: geometry.frameGrid,
476
- animations: options.asset.animations,
477
483
  settings: {
478
484
  ...options.asset.settings,
479
485
  format: normalizeAssetFormatFromMimeType(mimeType, options.file.name)
@@ -575,6 +581,13 @@ export async function spriteSheetFrameToDataUrl(options) {
575
581
  export function isSvgSource(src) {
576
582
  return src.startsWith("data:image/svg+xml") || /\.svg(?:$|\?)/i.test(src);
577
583
  }
584
+ const frameTouchUpPreferences = {
585
+ alpha: 255,
586
+ antiAlias: true,
587
+ brushSize: 4,
588
+ color: "#f8fafc",
589
+ tool: "brush"
590
+ };
578
591
  export async function openFrameTouchUpEditor(options) {
579
592
  const sourceImage = await loadImageElement(options.frameSrc);
580
593
  const dialog = document.createElement("div");
@@ -616,7 +629,7 @@ export async function openFrameTouchUpEditor(options) {
616
629
  brushSizeInput.min = "1";
617
630
  brushSizeInput.max = "32";
618
631
  brushSizeInput.step = "1";
619
- brushSizeInput.value = "4";
632
+ brushSizeInput.value = String(frameTouchUpPreferences.brushSize);
620
633
  const brushSizeValue = document.createElement("span");
621
634
  brushSizeValue.textContent = brushSizeInput.value;
622
635
  const brushSizeField = document.createElement("label");
@@ -624,7 +637,7 @@ export async function openFrameTouchUpEditor(options) {
624
637
  brushSizeField.append("Size", brushSizeInput, brushSizeValue);
625
638
  const antiAliasInput = document.createElement("input");
626
639
  antiAliasInput.type = "checkbox";
627
- antiAliasInput.checked = true;
640
+ antiAliasInput.checked = frameTouchUpPreferences.antiAlias;
628
641
  const antiAliasField = document.createElement("label");
629
642
  antiAliasField.className = "ai-game-assets-designer__inline-checkbox";
630
643
  antiAliasField.append(antiAliasInput, "Anti-aliased round tip");
@@ -640,14 +653,14 @@ export async function openFrameTouchUpEditor(options) {
640
653
  saveButton.classList.add("is-primary");
641
654
  const colorInput = document.createElement("input");
642
655
  colorInput.type = "color";
643
- colorInput.value = "#f8fafc";
656
+ colorInput.value = frameTouchUpPreferences.color;
644
657
  colorInput.setAttribute("aria-label", "Brush color");
645
658
  const alphaInput = document.createElement("input");
646
659
  alphaInput.type = "number";
647
660
  alphaInput.min = "0";
648
661
  alphaInput.max = "255";
649
662
  alphaInput.step = "1";
650
- alphaInput.value = "255";
663
+ alphaInput.value = String(frameTouchUpPreferences.alpha);
651
664
  alphaInput.setAttribute("aria-label", "Alpha");
652
665
  const rgbaLabel = document.createElement("label");
653
666
  rgbaLabel.className = "ai-game-assets-designer__touchup-color";
@@ -699,9 +712,10 @@ export async function openFrameTouchUpEditor(options) {
699
712
  context.clearRect(0, 0, canvas.width, canvas.height);
700
713
  context.drawImage(sourceImage, 0, 0, canvas.width, canvas.height);
701
714
  let zoom = Math.max(1, Math.floor(Math.min(8, 480 / Math.max(canvas.width, canvas.height))));
702
- let tool = "brush";
715
+ let tool = frameTouchUpPreferences.tool;
703
716
  let dirty = false;
704
717
  let drawing = false;
718
+ let drawingPointerId;
705
719
  let selectionStart;
706
720
  let selection;
707
721
  let animationTimeout;
@@ -751,6 +765,11 @@ export async function openFrameTouchUpEditor(options) {
751
765
  }[tool];
752
766
  activeButton.classList.add("is-active");
753
767
  };
768
+ const selectTool = (nextTool) => {
769
+ tool = nextTool;
770
+ frameTouchUpPreferences.tool = nextTool;
771
+ updateToolButtons();
772
+ };
754
773
  const updateUndoRedo = () => {
755
774
  undoButton.disabled = undoStack.length === 0;
756
775
  redoButton.disabled = redoStack.length === 0;
@@ -769,6 +788,22 @@ export async function openFrameTouchUpEditor(options) {
769
788
  redrawEditor();
770
789
  setDirty(true);
771
790
  };
791
+ const undo = () => {
792
+ const previous = undoStack.pop();
793
+ if (!previous)
794
+ return;
795
+ redoStack.push(snapshot());
796
+ restore(previous);
797
+ updateUndoRedo();
798
+ };
799
+ const redo = () => {
800
+ const next = redoStack.pop();
801
+ if (!next)
802
+ return;
803
+ undoStack.push(snapshot());
804
+ restore(next);
805
+ updateUndoRedo();
806
+ };
772
807
  const syncSelectionBox = () => {
773
808
  if (!selection) {
774
809
  selectionBox.hidden = true;
@@ -913,6 +948,8 @@ export async function openFrameTouchUpEditor(options) {
913
948
  const data = context.getImageData(point.x, point.y, 1, 1).data;
914
949
  colorInput.value = `#${hexByte(data[0])}${hexByte(data[1])}${hexByte(data[2])}`;
915
950
  alphaInput.value = String(data[3]);
951
+ frameTouchUpPreferences.color = colorInput.value;
952
+ frameTouchUpPreferences.alpha = data[3];
916
953
  };
917
954
  const fillAt = (point) => {
918
955
  const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
@@ -1020,7 +1057,10 @@ export async function openFrameTouchUpEditor(options) {
1020
1057
  disposed = true;
1021
1058
  if (animationTimeout !== undefined)
1022
1059
  window.clearTimeout(animationTimeout);
1023
- window.removeEventListener("keydown", keyHandler);
1060
+ window.removeEventListener("keydown", keyHandler, true);
1061
+ window.removeEventListener("pointerup", finishPointer, true);
1062
+ window.removeEventListener("pointercancel", finishPointer, true);
1063
+ window.removeEventListener("blur", cancelDrawing);
1024
1064
  document.removeEventListener("pointerdown", closeBrushMenuOnOutsidePointer);
1025
1065
  dialog.remove();
1026
1066
  };
@@ -1032,33 +1072,55 @@ export async function openFrameTouchUpEditor(options) {
1032
1072
  const keyHandler = (event) => {
1033
1073
  if (!dialog.isConnected)
1034
1074
  return;
1075
+ const key = event.key.toLowerCase();
1076
+ const commandModifier = event.metaKey || event.ctrlKey;
1077
+ if (commandModifier && !event.altKey && (key === "z" || key === "y")) {
1078
+ event.preventDefault();
1079
+ event.stopPropagation();
1080
+ if (key === "y" || event.shiftKey) {
1081
+ redo();
1082
+ }
1083
+ else {
1084
+ undo();
1085
+ }
1086
+ return;
1087
+ }
1035
1088
  if (event.key === "Escape") {
1089
+ event.preventDefault();
1090
+ event.stopPropagation();
1036
1091
  requestClose();
1037
1092
  return;
1038
1093
  }
1039
1094
  if (event.key === "Backspace" || event.key === "Delete") {
1040
1095
  event.preventDefault();
1096
+ event.stopPropagation();
1041
1097
  deleteSelection();
1042
1098
  return;
1043
1099
  }
1044
1100
  if (event.key === "ArrowLeft") {
1045
1101
  event.preventDefault();
1102
+ event.stopPropagation();
1046
1103
  moveSelection(-1, 0);
1047
1104
  }
1048
1105
  else if (event.key === "ArrowRight") {
1049
1106
  event.preventDefault();
1107
+ event.stopPropagation();
1050
1108
  moveSelection(1, 0);
1051
1109
  }
1052
1110
  else if (event.key === "ArrowUp") {
1053
1111
  event.preventDefault();
1112
+ event.stopPropagation();
1054
1113
  moveSelection(0, -1);
1055
1114
  }
1056
1115
  else if (event.key === "ArrowDown") {
1057
1116
  event.preventDefault();
1117
+ event.stopPropagation();
1058
1118
  moveSelection(0, 1);
1059
1119
  }
1060
1120
  };
1061
1121
  canvas.addEventListener("pointerdown", (event) => {
1122
+ if (event.pointerType !== "touch" && event.button !== 0)
1123
+ return;
1062
1124
  if (event.pointerType === "touch") {
1063
1125
  activeTouchPointers.set(event.pointerId, {
1064
1126
  x: event.clientX,
@@ -1066,6 +1128,7 @@ export async function openFrameTouchUpEditor(options) {
1066
1128
  });
1067
1129
  if (activeTouchPointers.size >= 2) {
1068
1130
  drawing = false;
1131
+ drawingPointerId = undefined;
1069
1132
  selectionStart = undefined;
1070
1133
  pinchStartDistance = touchPointerDistance();
1071
1134
  pinchStartZoom = zoom;
@@ -1080,6 +1143,7 @@ export async function openFrameTouchUpEditor(options) {
1080
1143
  }
1081
1144
  pushUndo();
1082
1145
  drawing = true;
1146
+ drawingPointerId = event.pointerId;
1083
1147
  canvas.setPointerCapture(event.pointerId);
1084
1148
  if (tool === "select") {
1085
1149
  selectionStart = point;
@@ -1090,6 +1154,7 @@ export async function openFrameTouchUpEditor(options) {
1090
1154
  if (tool === "fill") {
1091
1155
  fillAt(point);
1092
1156
  drawing = false;
1157
+ drawingPointerId = undefined;
1093
1158
  setDirty(true);
1094
1159
  redrawEditor();
1095
1160
  return;
@@ -1112,8 +1177,12 @@ export async function openFrameTouchUpEditor(options) {
1112
1177
  return;
1113
1178
  }
1114
1179
  }
1115
- if (!drawing)
1180
+ if (!drawing || drawingPointerId !== event.pointerId)
1181
+ return;
1182
+ if (event.pointerType !== "touch" && (event.buttons & 1) === 0) {
1183
+ finishPointer(event);
1116
1184
  return;
1185
+ }
1117
1186
  const point = canvasPoint(event);
1118
1187
  if (tool === "select" && selectionStart) {
1119
1188
  const x = Math.min(selectionStart.x, point.x);
@@ -1133,7 +1202,7 @@ export async function openFrameTouchUpEditor(options) {
1133
1202
  redrawEditor();
1134
1203
  }
1135
1204
  });
1136
- const finishPointer = (event) => {
1205
+ function finishPointer(event) {
1137
1206
  if (event.pointerType === "touch") {
1138
1207
  activeTouchPointers.delete(event.pointerId);
1139
1208
  if (activeTouchPointers.size < 2) {
@@ -1144,15 +1213,29 @@ export async function openFrameTouchUpEditor(options) {
1144
1213
  pinchStartZoom = zoom;
1145
1214
  }
1146
1215
  }
1147
- drawing = false;
1148
- selectionStart = undefined;
1216
+ if (drawingPointerId === event.pointerId) {
1217
+ drawing = false;
1218
+ drawingPointerId = undefined;
1219
+ selectionStart = undefined;
1220
+ }
1149
1221
  if (canvas.hasPointerCapture(event.pointerId)) {
1150
1222
  canvas.releasePointerCapture(event.pointerId);
1151
1223
  }
1152
1224
  redrawEditor();
1153
- };
1225
+ }
1226
+ function cancelDrawing() {
1227
+ drawing = false;
1228
+ drawingPointerId = undefined;
1229
+ selectionStart = undefined;
1230
+ activeTouchPointers.clear();
1231
+ pinchStartDistance = undefined;
1232
+ }
1154
1233
  canvas.addEventListener("pointerup", finishPointer);
1155
1234
  canvas.addEventListener("pointercancel", finishPointer);
1235
+ canvas.addEventListener("lostpointercapture", finishPointer);
1236
+ window.addEventListener("pointerup", finishPointer, true);
1237
+ window.addEventListener("pointercancel", finishPointer, true);
1238
+ window.addEventListener("blur", cancelDrawing);
1156
1239
  canvasWrap.addEventListener("wheel", (event) => {
1157
1240
  if (!event.ctrlKey && !event.metaKey)
1158
1241
  return;
@@ -1171,54 +1254,45 @@ export async function openFrameTouchUpEditor(options) {
1171
1254
  setZoom(zoom + 1);
1172
1255
  });
1173
1256
  brushButton.addEventListener("click", () => {
1174
- tool = "brush";
1175
- updateToolButtons();
1257
+ selectTool("brush");
1176
1258
  });
1177
1259
  brushMenuButton.addEventListener("click", () => {
1178
1260
  brushMenu.hidden = !brushMenu.hidden;
1179
1261
  });
1180
1262
  brushSizeInput.addEventListener("input", () => {
1181
1263
  brushSizeValue.textContent = brushSizeInput.value;
1264
+ frameTouchUpPreferences.brushSize = brushSize();
1265
+ });
1266
+ antiAliasInput.addEventListener("change", () => {
1267
+ frameTouchUpPreferences.antiAlias = antiAliasInput.checked;
1268
+ });
1269
+ colorInput.addEventListener("input", () => {
1270
+ frameTouchUpPreferences.color = colorInput.value;
1271
+ });
1272
+ alphaInput.addEventListener("input", () => {
1273
+ frameTouchUpPreferences.alpha = clamp(Math.round(Number(alphaInput.value)), 0, 255);
1182
1274
  });
1183
1275
  document.addEventListener("pointerdown", closeBrushMenuOnOutsidePointer);
1184
1276
  eraserButton.addEventListener("click", () => {
1185
- tool = "eraser";
1186
- updateToolButtons();
1277
+ selectTool("eraser");
1187
1278
  });
1188
1279
  pickerButton.addEventListener("click", () => {
1189
- tool = "picker";
1190
- updateToolButtons();
1280
+ selectTool("picker");
1191
1281
  });
1192
1282
  selectButton.addEventListener("click", () => {
1193
- tool = "select";
1194
- updateToolButtons();
1283
+ selectTool("select");
1195
1284
  });
1196
1285
  fillButton.addEventListener("click", () => {
1197
- tool = "fill";
1198
- updateToolButtons();
1199
- });
1200
- undoButton.addEventListener("click", () => {
1201
- const previous = undoStack.pop();
1202
- if (!previous)
1203
- return;
1204
- redoStack.push(snapshot());
1205
- restore(previous);
1206
- updateUndoRedo();
1207
- });
1208
- redoButton.addEventListener("click", () => {
1209
- const next = redoStack.pop();
1210
- if (!next)
1211
- return;
1212
- undoStack.push(snapshot());
1213
- restore(next);
1214
- updateUndoRedo();
1286
+ selectTool("fill");
1215
1287
  });
1288
+ undoButton.addEventListener("click", undo);
1289
+ redoButton.addEventListener("click", redo);
1216
1290
  saveButton.addEventListener("click", async () => {
1217
1291
  await options.onSave(canvas.toDataURL("image/png"));
1218
1292
  setDirty(false);
1219
1293
  close();
1220
1294
  });
1221
- window.addEventListener("keydown", keyHandler);
1295
+ window.addEventListener("keydown", keyHandler, true);
1222
1296
  updateZoom();
1223
1297
  updateToolButtons();
1224
1298
  updateUndoRedo();
@@ -1359,6 +1433,14 @@ export function renderAudioPlayer(options) {
1359
1433
  state.frame = window.requestAnimationFrame(tick);
1360
1434
  }
1361
1435
  };
1436
+ const handleEnded = () => {
1437
+ if (!options.playback?.loop || state.disposed) {
1438
+ sync();
1439
+ return;
1440
+ }
1441
+ audio.currentTime = state.trimStart;
1442
+ void audio.play().then(() => tick()).catch(() => sync());
1443
+ };
1362
1444
  playButton.addEventListener("click", async () => {
1363
1445
  if (audio.paused) {
1364
1446
  pauseSiblingAudioPlayers(options.container);
@@ -1393,12 +1475,15 @@ export function renderAudioPlayer(options) {
1393
1475
  state.trimStart = clamp(options.playback?.trimStartSeconds ?? 0, 0, state.duration);
1394
1476
  state.trimEnd = clamp(options.playback?.trimEndSeconds ?? state.duration, state.trimStart, state.duration);
1395
1477
  trimLabel.hidden = state.trimStart === 0 && state.trimEnd === state.duration;
1396
- trimLabel.textContent = `Trim ${formatAudioTime(state.trimStart)} – ${formatAudioTime(state.trimEnd)}`;
1478
+ trimLabel.textContent = `Trim ${formatAudioTime(state.trimStart, true)} – ${formatAudioTime(state.trimEnd, true)}`;
1479
+ audio.loop = Boolean(options.playback?.loop) &&
1480
+ state.trimStart === 0 &&
1481
+ state.trimEnd >= state.duration - 0.05;
1397
1482
  audio.currentTime = state.trimStart;
1398
1483
  sync();
1399
1484
  });
1400
1485
  audio.addEventListener("timeupdate", sync);
1401
- audio.addEventListener("ended", sync);
1486
+ audio.addEventListener("ended", handleEnded);
1402
1487
  draw();
1403
1488
  void audioWaveformPeaks(options.src).then((peaks) => {
1404
1489
  if (state.disposed)
@@ -1516,13 +1601,17 @@ export function progressForAudio(audio) {
1516
1601
  export function clamp(value, min, max) {
1517
1602
  return Math.min(max, Math.max(min, value));
1518
1603
  }
1519
- export function formatAudioTime(value) {
1604
+ export function formatAudioTime(value, fractional = false) {
1520
1605
  if (!Number.isFinite(value) || value < 0) {
1521
1606
  return "0:00";
1522
1607
  }
1523
- const minutes = Math.floor(value / 60);
1524
- const seconds = Math.floor(value % 60);
1525
- return `${minutes}:${String(seconds).padStart(2, "0")}`;
1608
+ const precision = fractional ? 2 : 0;
1609
+ const multiplier = 10 ** precision;
1610
+ const roundedValue = Math.round(value * multiplier) / multiplier;
1611
+ const minutes = Math.floor(roundedValue / 60);
1612
+ const seconds = roundedValue % 60;
1613
+ const formattedSeconds = seconds.toFixed(precision).padStart(fractional ? 5 : 2, "0");
1614
+ return `${minutes}:${formattedSeconds}`;
1526
1615
  }
1527
1616
  export function startSpritesheetPreview(options) {
1528
1617
  const frameGrid = options.asset.frameGrid;
@@ -2365,11 +2454,33 @@ export function setFrameBackground(element, options) {
2365
2454
  export function setFrameElementBackground(frameElement, options) {
2366
2455
  const column = options.frame % options.frameGrid.columns;
2367
2456
  const row = Math.floor(options.frame / options.frameGrid.columns);
2457
+ const margin = options.frameGrid.margin ?? 0;
2458
+ const spacing = options.frameGrid.spacing ?? 0;
2459
+ const frameWidth = options.frameGrid.frameWidth;
2460
+ const frameHeight = options.frameGrid.frameHeight;
2461
+ const scale = Math.min(options.displaySize.width / frameWidth, options.displaySize.height / frameHeight);
2462
+ const sheetWidth = (margin * 2) +
2463
+ (options.frameGrid.columns * frameWidth) +
2464
+ (Math.max(0, options.frameGrid.columns - 1) * spacing);
2465
+ const sheetHeight = (margin * 2) +
2466
+ (options.frameGrid.rows * frameHeight) +
2467
+ (Math.max(0, options.frameGrid.rows - 1) * spacing);
2468
+ const sourceX = margin + (column * (frameWidth + spacing));
2469
+ const sourceY = margin + (row * (frameHeight + spacing));
2470
+ const displayFrameWidth = frameWidth * scale;
2471
+ const displayFrameHeight = frameHeight * scale;
2472
+ const frameOffsetX = (options.displaySize.width - displayFrameWidth) / 2;
2473
+ const frameOffsetY = (options.displaySize.height - displayFrameHeight) / 2;
2474
+ frameElement.style.inset = "auto";
2475
+ frameElement.style.left = `${frameOffsetX}px`;
2476
+ frameElement.style.top = `${frameOffsetY}px`;
2477
+ frameElement.style.width = `${displayFrameWidth}px`;
2478
+ frameElement.style.height = `${displayFrameHeight}px`;
2368
2479
  frameElement.style.backgroundImage = `url("${cssUrl(options.src)}")`;
2369
2480
  frameElement.style.backgroundSize =
2370
- `${options.frameGrid.columns * options.displaySize.width}px ${options.frameGrid.rows * options.displaySize.height}px`;
2481
+ `${sheetWidth * scale}px ${sheetHeight * scale}px`;
2371
2482
  frameElement.style.backgroundPosition =
2372
- `${-(column * options.displaySize.width)}px ${-(row * options.displaySize.height)}px`;
2483
+ `${-(sourceX * scale)}px ${-(sourceY * scale)}px`;
2373
2484
  }
2374
2485
  export function ensureFramePreviewElement(element) {
2375
2486
  const existing = element.querySelector(":scope > .ai-game-assets-designer__frame-image");
@@ -2456,16 +2567,28 @@ export function isAudioAsset(asset) {
2456
2567
  export function isVoiceAsset(asset) {
2457
2568
  return asset?.kind === "voice" || asset?.kind === "voice-line";
2458
2569
  }
2459
- export function assetWithGeneratedGeometry(asset, option) {
2570
+ export function assetWithGeneratedGeometry(asset, option, options = {}) {
2460
2571
  return {
2461
2572
  ...asset,
2462
2573
  dimensions: option.dimensions ?? asset.dimensions,
2463
2574
  frameGrid: option.frameGrid ?? asset.frameGrid,
2464
- animations: option.animations ?? asset.animations,
2575
+ animations: option.animations ??
2576
+ (options.inheritAnimations
2577
+ ? asset.animations
2578
+ : candidateAnimationDefinitions(asset, option.frameGrid ?? asset.frameGrid)),
2465
2579
  audioPlayback: option.audioPlayback ?? asset.audioPlayback,
2466
2580
  voiceSettings: option.voiceSettings ?? asset.voiceSettings
2467
2581
  };
2468
2582
  }
2583
+ function candidateAnimationDefinitions(asset, frameGrid) {
2584
+ if (!frameGrid || !asset.animations?.length)
2585
+ return undefined;
2586
+ const frameCount = frameGrid.frameCount ?? frameGrid.columns * frameGrid.rows;
2587
+ const frames = Array.from({ length: frameCount }, (_, index) => index);
2588
+ // This is runtime-only playback structure, derived from the candidate's grid.
2589
+ // It deliberately does not copy prompt, timings, tags, or transforms.
2590
+ return asset.animations.map(({ key, frameRate, repeat }) => ({ key, frameRate, repeat, frames }));
2591
+ }
2469
2592
  export function positiveIntegerInput(input, fallback) {
2470
2593
  const value = Number(input.value);
2471
2594
  if (!Number.isFinite(value))
@@ -3074,6 +3197,7 @@ export function ensureDesignerStyles() {
3074
3197
  background: transparent;
3075
3198
  display: grid;
3076
3199
  place-items: center;
3200
+ overflow: hidden;
3077
3201
  cursor: pointer;
3078
3202
  color: #dbeafe;
3079
3203
  font: inherit;
@@ -3094,12 +3218,18 @@ export function ensureDesignerStyles() {
3094
3218
  object-fit: contain;
3095
3219
  image-rendering: pixelated;
3096
3220
  }
3221
+ .ai-game-assets-designer__option-animation {
3222
+ width: 68px;
3223
+ height: 68px;
3224
+ max-width: 100%;
3225
+ }
3097
3226
  .ai-game-assets-designer__option .ai-game-assets-designer__animate-button {
3098
3227
  width: 100%;
3099
3228
  }
3100
3229
  .ai-game-assets-designer__modal {
3101
3230
  position: fixed;
3102
3231
  inset: 0;
3232
+ z-index: 2147483647;
3103
3233
  display: grid;
3104
3234
  place-items: center;
3105
3235
  padding: 18px;
@@ -3231,14 +3361,24 @@ export function ensureDesignerStyles() {
3231
3361
  }
3232
3362
  .ai-game-assets-designer__frame-thumb {
3233
3363
  flex: 0 0 auto;
3234
- border: 2px solid #384251;
3364
+ border: 0;
3235
3365
  border-radius: 6px;
3236
3366
  background-color: #0f1218;
3237
3367
  position: relative;
3238
3368
  overflow: hidden;
3239
3369
  cursor: pointer;
3240
3370
  }
3241
- .ai-game-assets-designer__frame-thumb.is-selected {
3371
+ .ai-game-assets-designer__frame-thumb::after {
3372
+ content: "";
3373
+ position: absolute;
3374
+ inset: 0;
3375
+ z-index: 2;
3376
+ border: 2px solid #384251;
3377
+ border-radius: inherit;
3378
+ pointer-events: none;
3379
+ box-sizing: border-box;
3380
+ }
3381
+ .ai-game-assets-designer__frame-thumb.is-selected::after {
3242
3382
  border-color: #6ed3ff;
3243
3383
  }
3244
3384
  .ai-game-assets-designer__frame-fields {