@ai-game-assets/phaser 0.5.6 → 0.7.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.
- package/dist/animations.d.ts +2 -0
- package/dist/animations.d.ts.map +1 -1
- package/dist/animations.js +4 -3
- package/dist/animations.js.map +1 -1
- package/dist/debug-client.d.ts +52 -2
- package/dist/debug-client.d.ts.map +1 -1
- package/dist/debug-client.js +66 -0
- package/dist/debug-client.js.map +1 -1
- package/dist/designer-support.d.ts +31 -4
- package/dist/designer-support.d.ts.map +1 -1
- package/dist/designer-support.js +732 -80
- package/dist/designer-support.js.map +1 -1
- package/dist/designer.d.ts +19 -2
- package/dist/designer.d.ts.map +1 -1
- package/dist/designer.js +1735 -185
- package/dist/designer.js.map +1 -1
- package/dist/first-drafts.js +21 -9
- package/dist/first-drafts.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/keys.d.ts +2 -0
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +6 -0
- package/dist/keys.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +50 -3
- package/dist/loader.js.map +1 -1
- package/dist/phaser-types.d.ts +1 -0
- package/dist/phaser-types.d.ts.map +1 -1
- package/dist/runtime.d.ts +30 -2
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +235 -8
- package/dist/runtime.js.map +1 -1
- package/dist/tileset-dialog.d.ts +121 -0
- package/dist/tileset-dialog.d.ts.map +1 -0
- package/dist/tileset-dialog.js +1594 -0
- package/dist/tileset-dialog.js.map +1 -0
- package/dist/tilesets.d.ts +13 -0
- package/dist/tilesets.d.ts.map +1 -0
- package/dist/tilesets.js +60 -0
- package/dist/tilesets.js.map +1 -0
- package/package.json +3 -2
package/dist/designer-support.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { aiTextureKey } from "./keys.js";
|
|
1
2
|
const assetFormatOptions = [
|
|
2
3
|
{ value: "png", label: "PNG" },
|
|
3
4
|
{ value: "jpg", label: "JPEG" },
|
|
@@ -69,6 +70,19 @@ export function createDesignerElements(options, manifest, selectedAssetId) {
|
|
|
69
70
|
const promptInput = document.createElement("textarea");
|
|
70
71
|
promptInput.className = "ai-game-assets-designer__prompt";
|
|
71
72
|
promptInput.rows = 6;
|
|
73
|
+
const promptField = labelWrap("Prompt", promptInput);
|
|
74
|
+
const tilesetPromptsField = document.createElement("section");
|
|
75
|
+
tilesetPromptsField.className = "ai-game-assets-designer__tile-prompts";
|
|
76
|
+
tilesetPromptsField.hidden = true;
|
|
77
|
+
const tilesetPromptsTitle = document.createElement("div");
|
|
78
|
+
tilesetPromptsTitle.className = "ai-game-assets-designer__tile-prompts-title";
|
|
79
|
+
tilesetPromptsTitle.textContent = "Tile prompts";
|
|
80
|
+
const tilesetPromptsHint = document.createElement("div");
|
|
81
|
+
tilesetPromptsHint.className = "ai-game-assets-designer__tile-prompts-hint";
|
|
82
|
+
tilesetPromptsHint.textContent = "Describe each tile in exact sheet order.";
|
|
83
|
+
const tilesetPromptsList = document.createElement("div");
|
|
84
|
+
tilesetPromptsList.className = "ai-game-assets-designer__tile-prompts-list";
|
|
85
|
+
tilesetPromptsField.append(tilesetPromptsTitle, tilesetPromptsHint, tilesetPromptsList);
|
|
72
86
|
const widthInput = numericInput();
|
|
73
87
|
const heightInput = numericInput();
|
|
74
88
|
const frameCountInput = numericInput();
|
|
@@ -98,7 +112,9 @@ export function createDesignerElements(options, manifest, selectedAssetId) {
|
|
|
98
112
|
}
|
|
99
113
|
const dimensionGrid = document.createElement("div");
|
|
100
114
|
dimensionGrid.className = "ai-game-assets-designer__dimensions";
|
|
101
|
-
|
|
115
|
+
const widthField = labelWrap("Width", widthInput);
|
|
116
|
+
const heightField = labelWrap("Height", heightInput);
|
|
117
|
+
dimensionGrid.append(widthField, heightField);
|
|
102
118
|
const frameCountField = labelWrap("Frames", frameCountInput);
|
|
103
119
|
const formatField = labelWrap("Format", formatSelect);
|
|
104
120
|
const audioFormatField = labelWrap("Audio format", audioFormatSelect);
|
|
@@ -126,10 +142,19 @@ export function createDesignerElements(options, manifest, selectedAssetId) {
|
|
|
126
142
|
currentTouchUpButton.className = "ai-game-assets-designer__animate-button";
|
|
127
143
|
currentTouchUpButton.textContent = "Touch up...";
|
|
128
144
|
currentTouchUpButton.hidden = true;
|
|
129
|
-
|
|
145
|
+
const currentRevertButton = document.createElement("button");
|
|
146
|
+
currentRevertButton.type = "button";
|
|
147
|
+
currentRevertButton.className = "ai-game-assets-designer__animate-button";
|
|
148
|
+
currentRevertButton.textContent = "Revert preview";
|
|
149
|
+
currentRevertButton.hidden = true;
|
|
150
|
+
currentPreview.append(currentImage, currentAudio, currentAnimation, currentAnimationButton, currentTouchUpButton, currentRevertButton);
|
|
130
151
|
const regenerateButton = document.createElement("button");
|
|
131
152
|
regenerateButton.type = "button";
|
|
132
153
|
regenerateButton.textContent = "Regenerate";
|
|
154
|
+
const mixTilesetButton = document.createElement("button");
|
|
155
|
+
mixTilesetButton.type = "button";
|
|
156
|
+
mixTilesetButton.textContent = "Mix tileset";
|
|
157
|
+
mixTilesetButton.hidden = true;
|
|
133
158
|
const uploadButton = document.createElement("button");
|
|
134
159
|
uploadButton.type = "button";
|
|
135
160
|
uploadButton.textContent = "Upload...";
|
|
@@ -143,24 +168,29 @@ export function createDesignerElements(options, manifest, selectedAssetId) {
|
|
|
143
168
|
promoteButton.type = "button";
|
|
144
169
|
promoteButton.textContent = "Promote";
|
|
145
170
|
promoteButton.disabled = true;
|
|
171
|
+
const promoteAllButton = document.createElement("button");
|
|
172
|
+
promoteAllButton.type = "button";
|
|
173
|
+
promoteAllButton.textContent = "Promote all";
|
|
174
|
+
promoteAllButton.disabled = true;
|
|
146
175
|
const restartButton = document.createElement("button");
|
|
147
176
|
restartButton.type = "button";
|
|
148
177
|
restartButton.textContent = "Restart";
|
|
149
178
|
const actions = document.createElement("div");
|
|
150
179
|
actions.className = "ai-game-assets-designer__actions";
|
|
151
|
-
actions.append(regenerateButton, uploadButton, deriveButton, versionsButton, promoteButton, restartButton);
|
|
180
|
+
actions.append(regenerateButton, mixTilesetButton, uploadButton, deriveButton, versionsButton, promoteButton, promoteAllButton, restartButton);
|
|
152
181
|
const versionLabel = document.createElement("div");
|
|
153
182
|
versionLabel.className = "ai-game-assets-designer__meta";
|
|
154
183
|
const optionsGrid = document.createElement("div");
|
|
155
184
|
optionsGrid.className = "ai-game-assets-designer__options";
|
|
156
185
|
const status = document.createElement("div");
|
|
157
186
|
status.className = "ai-game-assets-designer__status";
|
|
158
|
-
panel.append(header, targetField, labelWrap("Asset", assetBrowser), targetVariantLabel, animationField, dimensionGrid, frameCountField, formatField, audioFormatField, audioDurationField, audioLoopField, voiceTextField, labelWrap("Current", currentPreview),
|
|
187
|
+
panel.append(header, targetField, labelWrap("Asset", assetBrowser), targetVariantLabel, animationField, dimensionGrid, frameCountField, formatField, audioFormatField, audioDurationField, audioLoopField, voiceTextField, labelWrap("Current", currentPreview), promptField, tilesetPromptsField, actions, versionLabel, optionsGrid, status);
|
|
159
188
|
root.append(toggle, panel);
|
|
160
189
|
return {
|
|
161
190
|
root,
|
|
162
191
|
toggle,
|
|
163
192
|
panel,
|
|
193
|
+
title,
|
|
164
194
|
styleButton,
|
|
165
195
|
assetSelect,
|
|
166
196
|
assetBrowser,
|
|
@@ -170,7 +200,9 @@ export function createDesignerElements(options, manifest, selectedAssetId) {
|
|
|
170
200
|
animationSelect,
|
|
171
201
|
animationField,
|
|
172
202
|
widthInput,
|
|
203
|
+
widthField,
|
|
173
204
|
heightInput,
|
|
205
|
+
heightField,
|
|
174
206
|
dimensionGrid,
|
|
175
207
|
frameCountInput,
|
|
176
208
|
frameCountField,
|
|
@@ -185,17 +217,23 @@ export function createDesignerElements(options, manifest, selectedAssetId) {
|
|
|
185
217
|
voiceTextInput,
|
|
186
218
|
voiceTextField,
|
|
187
219
|
promptInput,
|
|
220
|
+
promptField,
|
|
221
|
+
tilesetPromptsField,
|
|
222
|
+
tilesetPromptsList,
|
|
188
223
|
currentImage,
|
|
189
224
|
currentAudio,
|
|
190
225
|
currentAnimation,
|
|
191
226
|
currentAnimationButton,
|
|
227
|
+
currentRevertButton,
|
|
192
228
|
currentTouchUpButton,
|
|
193
229
|
currentPreview,
|
|
194
230
|
uploadButton,
|
|
195
231
|
deriveButton,
|
|
196
232
|
regenerateButton,
|
|
233
|
+
mixTilesetButton,
|
|
197
234
|
versionsButton,
|
|
198
235
|
promoteButton,
|
|
236
|
+
promoteAllButton,
|
|
199
237
|
restartButton,
|
|
200
238
|
versionLabel,
|
|
201
239
|
options: optionsGrid,
|
|
@@ -214,7 +252,7 @@ export function renderAssetFolderBrowser(options) {
|
|
|
214
252
|
const storedPathValue = options.container.getAttribute("data-path");
|
|
215
253
|
const storedPath = storedPathValue !== null
|
|
216
254
|
? storedPathValue.split("/").filter(Boolean)
|
|
217
|
-
:
|
|
255
|
+
: [];
|
|
218
256
|
const currentPath = pathHasAnyAsset(storedPath, assetPathEntries)
|
|
219
257
|
? storedPath
|
|
220
258
|
: selectedPath;
|
|
@@ -390,7 +428,7 @@ export function renderOptions(options) {
|
|
|
390
428
|
element: animationStage,
|
|
391
429
|
src: option.dataUrl,
|
|
392
430
|
asset: optionAsset,
|
|
393
|
-
displaySize:
|
|
431
|
+
displaySize: { width: 68, height: 68 },
|
|
394
432
|
applyFrameTransforms: false
|
|
395
433
|
});
|
|
396
434
|
});
|
|
@@ -399,6 +437,8 @@ export function renderOptions(options) {
|
|
|
399
437
|
options.elements.options.append(card);
|
|
400
438
|
}
|
|
401
439
|
}
|
|
440
|
+
const pendingImagePreviews = new WeakMap();
|
|
441
|
+
let promotedTextureInstallId = 0;
|
|
402
442
|
export function previewOption(options) {
|
|
403
443
|
const textureKey = `ai-preview:${options.assetId}:${options.option.index}:${Date.now()}`;
|
|
404
444
|
previewImageSource({
|
|
@@ -418,25 +458,80 @@ export function previewCurrentAsset(options) {
|
|
|
418
458
|
textureKey
|
|
419
459
|
});
|
|
420
460
|
}
|
|
461
|
+
export function installPromotedImageTexture(options) {
|
|
462
|
+
const textureKey = aiTextureKey({ assetId: options.assetId });
|
|
463
|
+
promotedTextureInstallId += 1;
|
|
464
|
+
const requestKey = `promote:${options.assetId}:${promotedTextureInstallId}`;
|
|
465
|
+
return new Promise((resolve, reject) => {
|
|
466
|
+
previewImageSource({
|
|
467
|
+
...options,
|
|
468
|
+
textureKey,
|
|
469
|
+
requestKey,
|
|
470
|
+
onPreview(assetId, installedTextureKey, asset) {
|
|
471
|
+
resolve({ assetId, textureKey: installedTextureKey, asset });
|
|
472
|
+
},
|
|
473
|
+
onError: reject
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
}
|
|
421
477
|
export function previewImageSource(options) {
|
|
422
478
|
const image = new Image();
|
|
479
|
+
const pendingForScene = pendingImagePreviews.get(options.scene) ?? new Map();
|
|
480
|
+
const requestKey = options.requestKey ?? options.assetId;
|
|
481
|
+
const previousImage = pendingForScene.get(requestKey);
|
|
482
|
+
if (previousImage) {
|
|
483
|
+
previousImage.onload = null;
|
|
484
|
+
previousImage.onerror = null;
|
|
485
|
+
}
|
|
486
|
+
pendingForScene.set(requestKey, image);
|
|
487
|
+
pendingImagePreviews.set(options.scene, pendingForScene);
|
|
488
|
+
if (shouldUseAnonymousCrossOrigin(options.src)) {
|
|
489
|
+
image.crossOrigin = "anonymous";
|
|
490
|
+
}
|
|
423
491
|
image.onload = () => {
|
|
424
|
-
if (
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
492
|
+
if (pendingForScene.get(requestKey) !== image)
|
|
493
|
+
return;
|
|
494
|
+
pendingForScene.delete(requestKey);
|
|
495
|
+
try {
|
|
496
|
+
if (options.scene.textures.exists(options.textureKey)) {
|
|
497
|
+
options.scene.textures.remove(options.textureKey);
|
|
498
|
+
}
|
|
499
|
+
const asset = options.assetOverride ?? options.manifest.assets[options.assetId];
|
|
500
|
+
const tileset = designerTilesetMetadata(asset);
|
|
501
|
+
if (tileset && options.scene.textures.addSpriteSheet) {
|
|
502
|
+
options.scene.textures.addSpriteSheet(options.textureKey, image, {
|
|
503
|
+
frameWidth: tileset.tileWidth,
|
|
504
|
+
frameHeight: tileset.tileHeight,
|
|
505
|
+
margin: tileset.margin,
|
|
506
|
+
spacing: tileset.spacing
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
else if (asset.frameGrid && options.scene.textures.addSpriteSheet) {
|
|
510
|
+
options.scene.textures.addSpriteSheet(options.textureKey, image, {
|
|
511
|
+
frameWidth: asset.frameGrid.frameWidth,
|
|
512
|
+
frameHeight: asset.frameGrid.frameHeight,
|
|
513
|
+
margin: asset.frameGrid.margin,
|
|
514
|
+
spacing: asset.frameGrid.spacing
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
options.scene.textures.addImage(options.textureKey, image);
|
|
519
|
+
}
|
|
520
|
+
options.onPreview(options.assetId, options.textureKey, asset);
|
|
435
521
|
}
|
|
436
|
-
|
|
437
|
-
|
|
522
|
+
catch (error) {
|
|
523
|
+
const previewError = error instanceof Error ? error : new Error(String(error));
|
|
524
|
+
if (options.onError)
|
|
525
|
+
options.onError(previewError);
|
|
526
|
+
else
|
|
527
|
+
console.error("Could not preview AI asset image.", previewError);
|
|
438
528
|
}
|
|
439
|
-
|
|
529
|
+
};
|
|
530
|
+
image.onerror = () => {
|
|
531
|
+
if (pendingForScene.get(requestKey) === image) {
|
|
532
|
+
pendingForScene.delete(requestKey);
|
|
533
|
+
}
|
|
534
|
+
options.onError?.(new Error(`Could not load preview image for "${options.assetId}".`));
|
|
440
535
|
};
|
|
441
536
|
image.src = options.src;
|
|
442
537
|
}
|
|
@@ -464,7 +559,9 @@ export async function uploadedOptionFromFile(options) {
|
|
|
464
559
|
};
|
|
465
560
|
}
|
|
466
561
|
const imageSize = await imageSizeFromSource(dataUrl);
|
|
467
|
-
const
|
|
562
|
+
const format = normalizeAssetFormatFromMimeType(mimeType, options.file.name);
|
|
563
|
+
const generationGeometry = generationOverridesFromInputs(options.elements, options.asset, format);
|
|
564
|
+
const geometry = uploadedImageGeometry(options.asset, imageSize, generationGeometry.tileset);
|
|
468
565
|
return {
|
|
469
566
|
index: 0,
|
|
470
567
|
dataUrl,
|
|
@@ -473,13 +570,38 @@ export async function uploadedOptionFromFile(options) {
|
|
|
473
570
|
model: "uploaded",
|
|
474
571
|
dimensions: geometry.dimensions,
|
|
475
572
|
frameGrid: geometry.frameGrid,
|
|
573
|
+
tileset: geometry.tileset,
|
|
476
574
|
settings: {
|
|
477
575
|
...options.asset.settings,
|
|
478
|
-
format
|
|
576
|
+
format
|
|
479
577
|
}
|
|
480
578
|
};
|
|
481
579
|
}
|
|
482
|
-
export function uploadedImageGeometry(asset, imageSize) {
|
|
580
|
+
export function uploadedImageGeometry(asset, imageSize, tilesetOverride) {
|
|
581
|
+
const baseTileset = designerTilesetMetadata(asset);
|
|
582
|
+
if (baseTileset) {
|
|
583
|
+
const tileset = {
|
|
584
|
+
...baseTileset,
|
|
585
|
+
...tilesetOverride
|
|
586
|
+
};
|
|
587
|
+
const capacity = tileset.columns * tileset.rows;
|
|
588
|
+
const tileCount = tileset.tileCount ?? capacity;
|
|
589
|
+
if (tileCount > capacity) {
|
|
590
|
+
throw new Error(`Tile count must not exceed this tileset's ${capacity}-tile capacity.`);
|
|
591
|
+
}
|
|
592
|
+
const margin = tileset.margin ?? 0;
|
|
593
|
+
const spacing = tileset.spacing ?? 0;
|
|
594
|
+
const dimensions = {
|
|
595
|
+
width: margin * 2 + tileset.columns * tileset.tileWidth +
|
|
596
|
+
Math.max(0, tileset.columns - 1) * spacing,
|
|
597
|
+
height: margin * 2 + tileset.rows * tileset.tileHeight +
|
|
598
|
+
Math.max(0, tileset.rows - 1) * spacing
|
|
599
|
+
};
|
|
600
|
+
if (imageSize.width !== dimensions.width || imageSize.height !== dimensions.height) {
|
|
601
|
+
throw new Error(`Tileset upload must be exactly ${dimensions.width}x${dimensions.height}px for the selected tile geometry; received ${imageSize.width}x${imageSize.height}px.`);
|
|
602
|
+
}
|
|
603
|
+
return { dimensions, tileset };
|
|
604
|
+
}
|
|
483
605
|
if (!asset.frameGrid) {
|
|
484
606
|
return {
|
|
485
607
|
dimensions: imageSize
|
|
@@ -574,6 +696,14 @@ export async function spriteSheetFrameToDataUrl(options) {
|
|
|
574
696
|
export function isSvgSource(src) {
|
|
575
697
|
return src.startsWith("data:image/svg+xml") || /\.svg(?:$|\?)/i.test(src);
|
|
576
698
|
}
|
|
699
|
+
const frameTouchUpPreferences = {
|
|
700
|
+
alpha: 255,
|
|
701
|
+
antiAlias: true,
|
|
702
|
+
brushSize: 4,
|
|
703
|
+
color: "#f8fafc",
|
|
704
|
+
tool: "brush"
|
|
705
|
+
};
|
|
706
|
+
let frameTouchUpClipboard;
|
|
577
707
|
export async function openFrameTouchUpEditor(options) {
|
|
578
708
|
const sourceImage = await loadImageElement(options.frameSrc);
|
|
579
709
|
const dialog = document.createElement("div");
|
|
@@ -615,7 +745,7 @@ export async function openFrameTouchUpEditor(options) {
|
|
|
615
745
|
brushSizeInput.min = "1";
|
|
616
746
|
brushSizeInput.max = "32";
|
|
617
747
|
brushSizeInput.step = "1";
|
|
618
|
-
brushSizeInput.value =
|
|
748
|
+
brushSizeInput.value = String(frameTouchUpPreferences.brushSize);
|
|
619
749
|
const brushSizeValue = document.createElement("span");
|
|
620
750
|
brushSizeValue.textContent = brushSizeInput.value;
|
|
621
751
|
const brushSizeField = document.createElement("label");
|
|
@@ -623,7 +753,7 @@ export async function openFrameTouchUpEditor(options) {
|
|
|
623
753
|
brushSizeField.append("Size", brushSizeInput, brushSizeValue);
|
|
624
754
|
const antiAliasInput = document.createElement("input");
|
|
625
755
|
antiAliasInput.type = "checkbox";
|
|
626
|
-
antiAliasInput.checked =
|
|
756
|
+
antiAliasInput.checked = frameTouchUpPreferences.antiAlias;
|
|
627
757
|
const antiAliasField = document.createElement("label");
|
|
628
758
|
antiAliasField.className = "ai-game-assets-designer__inline-checkbox";
|
|
629
759
|
antiAliasField.append(antiAliasInput, "Anti-aliased round tip");
|
|
@@ -632,6 +762,7 @@ export async function openFrameTouchUpEditor(options) {
|
|
|
632
762
|
const eraserButton = touchUpButton("Eraser");
|
|
633
763
|
const pickerButton = touchUpButton("Picker");
|
|
634
764
|
const selectButton = touchUpButton("Select");
|
|
765
|
+
selectButton.title = "Select pixels. Cmd/Ctrl+C copies; Cmd/Ctrl+V pastes.";
|
|
635
766
|
const fillButton = touchUpButton("Fill");
|
|
636
767
|
const undoButton = touchUpButton("Undo");
|
|
637
768
|
const redoButton = touchUpButton("Redo");
|
|
@@ -639,14 +770,14 @@ export async function openFrameTouchUpEditor(options) {
|
|
|
639
770
|
saveButton.classList.add("is-primary");
|
|
640
771
|
const colorInput = document.createElement("input");
|
|
641
772
|
colorInput.type = "color";
|
|
642
|
-
colorInput.value =
|
|
773
|
+
colorInput.value = frameTouchUpPreferences.color;
|
|
643
774
|
colorInput.setAttribute("aria-label", "Brush color");
|
|
644
775
|
const alphaInput = document.createElement("input");
|
|
645
776
|
alphaInput.type = "number";
|
|
646
777
|
alphaInput.min = "0";
|
|
647
778
|
alphaInput.max = "255";
|
|
648
779
|
alphaInput.step = "1";
|
|
649
|
-
alphaInput.value =
|
|
780
|
+
alphaInput.value = String(frameTouchUpPreferences.alpha);
|
|
650
781
|
alphaInput.setAttribute("aria-label", "Alpha");
|
|
651
782
|
const rgbaLabel = document.createElement("label");
|
|
652
783
|
rgbaLabel.className = "ai-game-assets-designer__touchup-color";
|
|
@@ -665,6 +796,13 @@ export async function openFrameTouchUpEditor(options) {
|
|
|
665
796
|
const selectionBox = document.createElement("div");
|
|
666
797
|
selectionBox.className = "ai-game-assets-designer__touchup-selection";
|
|
667
798
|
selectionBox.hidden = true;
|
|
799
|
+
const rotateSelectionButton = document.createElement("button");
|
|
800
|
+
rotateSelectionButton.type = "button";
|
|
801
|
+
rotateSelectionButton.className = "ai-game-assets-designer__touchup-selection-rotate";
|
|
802
|
+
rotateSelectionButton.textContent = "↻";
|
|
803
|
+
rotateSelectionButton.setAttribute("aria-label", "Rotate selection 90 degrees clockwise");
|
|
804
|
+
rotateSelectionButton.title = "Rotate selection 90° clockwise";
|
|
805
|
+
selectionBox.append(rotateSelectionButton);
|
|
668
806
|
canvasWrap.append(canvas, selectionBox);
|
|
669
807
|
const side = document.createElement("div");
|
|
670
808
|
side.className = "ai-game-assets-designer__touchup-side";
|
|
@@ -698,11 +836,13 @@ export async function openFrameTouchUpEditor(options) {
|
|
|
698
836
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
699
837
|
context.drawImage(sourceImage, 0, 0, canvas.width, canvas.height);
|
|
700
838
|
let zoom = Math.max(1, Math.floor(Math.min(8, 480 / Math.max(canvas.width, canvas.height))));
|
|
701
|
-
let tool =
|
|
839
|
+
let tool = frameTouchUpPreferences.tool;
|
|
702
840
|
let dirty = false;
|
|
703
841
|
let drawing = false;
|
|
842
|
+
let drawingPointerId;
|
|
704
843
|
let selectionStart;
|
|
705
844
|
let selection;
|
|
845
|
+
let floatingSelection;
|
|
706
846
|
let animationTimeout;
|
|
707
847
|
let animationCursor = 0;
|
|
708
848
|
let disposed = false;
|
|
@@ -750,11 +890,73 @@ export async function openFrameTouchUpEditor(options) {
|
|
|
750
890
|
}[tool];
|
|
751
891
|
activeButton.classList.add("is-active");
|
|
752
892
|
};
|
|
893
|
+
const selectTool = (nextTool) => {
|
|
894
|
+
tool = nextTool;
|
|
895
|
+
frameTouchUpPreferences.tool = nextTool;
|
|
896
|
+
updateToolButtons();
|
|
897
|
+
};
|
|
753
898
|
const updateUndoRedo = () => {
|
|
754
899
|
undoButton.disabled = undoStack.length === 0;
|
|
755
900
|
redoButton.disabled = redoStack.length === 0;
|
|
756
901
|
};
|
|
757
902
|
const snapshot = () => context.getImageData(0, 0, canvas.width, canvas.height);
|
|
903
|
+
const clipboardImageData = () => {
|
|
904
|
+
if (!frameTouchUpClipboard)
|
|
905
|
+
return undefined;
|
|
906
|
+
const width = Math.min(frameTouchUpClipboard.width, canvas.width);
|
|
907
|
+
const height = Math.min(frameTouchUpClipboard.height, canvas.height);
|
|
908
|
+
const imageData = context.createImageData(width, height);
|
|
909
|
+
const sourceStride = frameTouchUpClipboard.width * 4;
|
|
910
|
+
const targetStride = width * 4;
|
|
911
|
+
for (let row = 0; row < height; row += 1) {
|
|
912
|
+
imageData.data.set(frameTouchUpClipboard.pixels.subarray(row * sourceStride, (row * sourceStride) + targetStride), row * targetStride);
|
|
913
|
+
}
|
|
914
|
+
return imageData;
|
|
915
|
+
};
|
|
916
|
+
const drawFloatingSelection = () => {
|
|
917
|
+
if (!selection || !floatingSelection)
|
|
918
|
+
return;
|
|
919
|
+
context.putImageData(floatingSelection.background, 0, 0);
|
|
920
|
+
const floatingCanvas = document.createElement("canvas");
|
|
921
|
+
floatingCanvas.width = floatingSelection.pixels.width;
|
|
922
|
+
floatingCanvas.height = floatingSelection.pixels.height;
|
|
923
|
+
const floatingContext = floatingCanvas.getContext("2d");
|
|
924
|
+
if (!floatingContext)
|
|
925
|
+
return;
|
|
926
|
+
floatingContext.putImageData(floatingSelection.pixels, 0, 0);
|
|
927
|
+
context.drawImage(floatingCanvas, selection.x, selection.y);
|
|
928
|
+
};
|
|
929
|
+
const rotateImageDataWithinBounds = (imageData) => {
|
|
930
|
+
const sourceCanvas = document.createElement("canvas");
|
|
931
|
+
sourceCanvas.width = imageData.width;
|
|
932
|
+
sourceCanvas.height = imageData.height;
|
|
933
|
+
const sourceContext = sourceCanvas.getContext("2d");
|
|
934
|
+
const targetCanvas = document.createElement("canvas");
|
|
935
|
+
targetCanvas.width = imageData.width;
|
|
936
|
+
targetCanvas.height = imageData.height;
|
|
937
|
+
const targetContext = targetCanvas.getContext("2d", { willReadFrequently: true });
|
|
938
|
+
if (!sourceContext || !targetContext)
|
|
939
|
+
return imageData;
|
|
940
|
+
sourceContext.putImageData(imageData, 0, 0);
|
|
941
|
+
targetContext.imageSmoothingEnabled = false;
|
|
942
|
+
targetContext.translate(imageData.width / 2, imageData.height / 2);
|
|
943
|
+
targetContext.scale(imageData.width / imageData.height, imageData.height / imageData.width);
|
|
944
|
+
targetContext.rotate(Math.PI / 2);
|
|
945
|
+
targetContext.drawImage(sourceCanvas, -imageData.width / 2, -imageData.height / 2);
|
|
946
|
+
targetContext.setTransform(1, 0, 0, 1, 0, 0);
|
|
947
|
+
return targetContext.getImageData(0, 0, imageData.width, imageData.height);
|
|
948
|
+
};
|
|
949
|
+
const copySelection = () => {
|
|
950
|
+
if (!selection)
|
|
951
|
+
return false;
|
|
952
|
+
const imageData = floatingSelection?.pixels ?? context.getImageData(selection.x, selection.y, selection.width, selection.height);
|
|
953
|
+
frameTouchUpClipboard = {
|
|
954
|
+
width: imageData.width,
|
|
955
|
+
height: imageData.height,
|
|
956
|
+
pixels: new Uint8ClampedArray(imageData.data)
|
|
957
|
+
};
|
|
958
|
+
return true;
|
|
959
|
+
};
|
|
758
960
|
const pushUndo = () => {
|
|
759
961
|
undoStack.push(snapshot());
|
|
760
962
|
if (undoStack.length > 60)
|
|
@@ -765,9 +967,26 @@ export async function openFrameTouchUpEditor(options) {
|
|
|
765
967
|
const restore = (imageData) => {
|
|
766
968
|
context.putImageData(imageData, 0, 0);
|
|
767
969
|
selection = undefined;
|
|
970
|
+
floatingSelection = undefined;
|
|
768
971
|
redrawEditor();
|
|
769
972
|
setDirty(true);
|
|
770
973
|
};
|
|
974
|
+
const undo = () => {
|
|
975
|
+
const previous = undoStack.pop();
|
|
976
|
+
if (!previous)
|
|
977
|
+
return;
|
|
978
|
+
redoStack.push(snapshot());
|
|
979
|
+
restore(previous);
|
|
980
|
+
updateUndoRedo();
|
|
981
|
+
};
|
|
982
|
+
const redo = () => {
|
|
983
|
+
const next = redoStack.pop();
|
|
984
|
+
if (!next)
|
|
985
|
+
return;
|
|
986
|
+
undoStack.push(snapshot());
|
|
987
|
+
restore(next);
|
|
988
|
+
updateUndoRedo();
|
|
989
|
+
};
|
|
771
990
|
const syncSelectionBox = () => {
|
|
772
991
|
if (!selection) {
|
|
773
992
|
selectionBox.hidden = true;
|
|
@@ -778,6 +997,12 @@ export async function openFrameTouchUpEditor(options) {
|
|
|
778
997
|
selectionBox.style.top = `${selection.y * zoom}px`;
|
|
779
998
|
selectionBox.style.width = `${selection.width * zoom}px`;
|
|
780
999
|
selectionBox.style.height = `${selection.height * zoom}px`;
|
|
1000
|
+
const renderedWidth = selection.width * zoom;
|
|
1001
|
+
const renderedHeight = selection.height * zoom;
|
|
1002
|
+
rotateSelectionButton.hidden = renderedWidth < 12 || renderedHeight < 12;
|
|
1003
|
+
const handleSize = Math.max(0, Math.min(18, renderedWidth - 4, renderedHeight - 4));
|
|
1004
|
+
rotateSelectionButton.style.width = `${handleSize}px`;
|
|
1005
|
+
rotateSelectionButton.style.height = `${handleSize}px`;
|
|
781
1006
|
};
|
|
782
1007
|
const refreshPreviews = () => {
|
|
783
1008
|
stillContext.clearRect(0, 0, stillPreview.width, stillPreview.height);
|
|
@@ -912,6 +1137,8 @@ export async function openFrameTouchUpEditor(options) {
|
|
|
912
1137
|
const data = context.getImageData(point.x, point.y, 1, 1).data;
|
|
913
1138
|
colorInput.value = `#${hexByte(data[0])}${hexByte(data[1])}${hexByte(data[2])}`;
|
|
914
1139
|
alphaInput.value = String(data[3]);
|
|
1140
|
+
frameTouchUpPreferences.color = colorInput.value;
|
|
1141
|
+
frameTouchUpPreferences.alpha = data[3];
|
|
915
1142
|
};
|
|
916
1143
|
const fillAt = (point) => {
|
|
917
1144
|
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
|
@@ -959,6 +1186,13 @@ export async function openFrameTouchUpEditor(options) {
|
|
|
959
1186
|
pushUndo();
|
|
960
1187
|
const nextX = clamp(selection.x + deltaX, 0, canvas.width - selection.width);
|
|
961
1188
|
const nextY = clamp(selection.y + deltaY, 0, canvas.height - selection.height);
|
|
1189
|
+
if (floatingSelection) {
|
|
1190
|
+
selection = { ...selection, x: nextX, y: nextY };
|
|
1191
|
+
drawFloatingSelection();
|
|
1192
|
+
setDirty(true);
|
|
1193
|
+
redrawEditor();
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
962
1196
|
const imageData = context.getImageData(selection.x, selection.y, selection.width, selection.height);
|
|
963
1197
|
context.clearRect(selection.x, selection.y, selection.width, selection.height);
|
|
964
1198
|
context.putImageData(imageData, nextX, nextY);
|
|
@@ -966,15 +1200,60 @@ export async function openFrameTouchUpEditor(options) {
|
|
|
966
1200
|
setDirty(true);
|
|
967
1201
|
redrawEditor();
|
|
968
1202
|
};
|
|
1203
|
+
const rotateSelection = () => {
|
|
1204
|
+
if (!selection)
|
|
1205
|
+
return;
|
|
1206
|
+
pushUndo();
|
|
1207
|
+
const pixels = floatingSelection?.pixels ?? context.getImageData(selection.x, selection.y, selection.width, selection.height);
|
|
1208
|
+
const rotated = rotateImageDataWithinBounds(pixels);
|
|
1209
|
+
if (floatingSelection) {
|
|
1210
|
+
floatingSelection.pixels = rotated;
|
|
1211
|
+
drawFloatingSelection();
|
|
1212
|
+
}
|
|
1213
|
+
else {
|
|
1214
|
+
context.putImageData(rotated, selection.x, selection.y);
|
|
1215
|
+
}
|
|
1216
|
+
setDirty(true);
|
|
1217
|
+
redrawEditor();
|
|
1218
|
+
};
|
|
969
1219
|
const deleteSelection = () => {
|
|
970
1220
|
if (!selection)
|
|
971
1221
|
return;
|
|
972
1222
|
pushUndo();
|
|
973
|
-
|
|
1223
|
+
if (floatingSelection) {
|
|
1224
|
+
context.putImageData(floatingSelection.background, 0, 0);
|
|
1225
|
+
floatingSelection = undefined;
|
|
1226
|
+
}
|
|
1227
|
+
else {
|
|
1228
|
+
context.clearRect(selection.x, selection.y, selection.width, selection.height);
|
|
1229
|
+
}
|
|
974
1230
|
selection = undefined;
|
|
975
1231
|
setDirty(true);
|
|
976
1232
|
redrawEditor();
|
|
977
1233
|
};
|
|
1234
|
+
const pasteSelection = () => {
|
|
1235
|
+
const pixels = clipboardImageData();
|
|
1236
|
+
if (!pixels)
|
|
1237
|
+
return false;
|
|
1238
|
+
const x = clamp(selection?.x ?? Math.floor((canvas.width - pixels.width) / 2), 0, canvas.width - pixels.width);
|
|
1239
|
+
const y = clamp(selection?.y ?? Math.floor((canvas.height - pixels.height) / 2), 0, canvas.height - pixels.height);
|
|
1240
|
+
pushUndo();
|
|
1241
|
+
floatingSelection = {
|
|
1242
|
+
background: snapshot(),
|
|
1243
|
+
pixels
|
|
1244
|
+
};
|
|
1245
|
+
selection = {
|
|
1246
|
+
x,
|
|
1247
|
+
y,
|
|
1248
|
+
width: pixels.width,
|
|
1249
|
+
height: pixels.height
|
|
1250
|
+
};
|
|
1251
|
+
selectTool("select");
|
|
1252
|
+
drawFloatingSelection();
|
|
1253
|
+
setDirty(true);
|
|
1254
|
+
redrawEditor();
|
|
1255
|
+
return true;
|
|
1256
|
+
};
|
|
978
1257
|
const drawAnimationFrame = () => {
|
|
979
1258
|
if (disposed)
|
|
980
1259
|
return;
|
|
@@ -1016,12 +1295,18 @@ export async function openFrameTouchUpEditor(options) {
|
|
|
1016
1295
|
brushMenu.hidden = true;
|
|
1017
1296
|
};
|
|
1018
1297
|
const close = () => {
|
|
1298
|
+
if (disposed)
|
|
1299
|
+
return;
|
|
1019
1300
|
disposed = true;
|
|
1020
1301
|
if (animationTimeout !== undefined)
|
|
1021
1302
|
window.clearTimeout(animationTimeout);
|
|
1022
|
-
window.removeEventListener("keydown", keyHandler);
|
|
1303
|
+
window.removeEventListener("keydown", keyHandler, true);
|
|
1304
|
+
window.removeEventListener("pointerup", finishPointer, true);
|
|
1305
|
+
window.removeEventListener("pointercancel", finishPointer, true);
|
|
1306
|
+
window.removeEventListener("blur", cancelDrawing);
|
|
1023
1307
|
document.removeEventListener("pointerdown", closeBrushMenuOnOutsidePointer);
|
|
1024
1308
|
dialog.remove();
|
|
1309
|
+
options.onClose?.();
|
|
1025
1310
|
};
|
|
1026
1311
|
const requestClose = () => {
|
|
1027
1312
|
if (dirty && !window.confirm("You have unsaved changes. Close without saving?"))
|
|
@@ -1031,33 +1316,72 @@ export async function openFrameTouchUpEditor(options) {
|
|
|
1031
1316
|
const keyHandler = (event) => {
|
|
1032
1317
|
if (!dialog.isConnected)
|
|
1033
1318
|
return;
|
|
1319
|
+
const key = event.key.toLowerCase();
|
|
1320
|
+
const commandModifier = event.metaKey || event.ctrlKey;
|
|
1321
|
+
const target = event.target;
|
|
1322
|
+
const isEditingInput = target instanceof HTMLInputElement ||
|
|
1323
|
+
target instanceof HTMLTextAreaElement ||
|
|
1324
|
+
target instanceof HTMLSelectElement ||
|
|
1325
|
+
(target instanceof HTMLElement && target.isContentEditable);
|
|
1326
|
+
if (isEditingInput && event.key !== "Escape")
|
|
1327
|
+
return;
|
|
1328
|
+
if (commandModifier && !event.altKey && key === "c" && copySelection()) {
|
|
1329
|
+
event.preventDefault();
|
|
1330
|
+
event.stopPropagation();
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
if (commandModifier && !event.altKey && key === "v" && pasteSelection()) {
|
|
1334
|
+
event.preventDefault();
|
|
1335
|
+
event.stopPropagation();
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
if (commandModifier && !event.altKey && (key === "z" || key === "y")) {
|
|
1339
|
+
event.preventDefault();
|
|
1340
|
+
event.stopPropagation();
|
|
1341
|
+
if (key === "y" || event.shiftKey) {
|
|
1342
|
+
redo();
|
|
1343
|
+
}
|
|
1344
|
+
else {
|
|
1345
|
+
undo();
|
|
1346
|
+
}
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1034
1349
|
if (event.key === "Escape") {
|
|
1350
|
+
event.preventDefault();
|
|
1351
|
+
event.stopPropagation();
|
|
1035
1352
|
requestClose();
|
|
1036
1353
|
return;
|
|
1037
1354
|
}
|
|
1038
1355
|
if (event.key === "Backspace" || event.key === "Delete") {
|
|
1039
1356
|
event.preventDefault();
|
|
1357
|
+
event.stopPropagation();
|
|
1040
1358
|
deleteSelection();
|
|
1041
1359
|
return;
|
|
1042
1360
|
}
|
|
1043
1361
|
if (event.key === "ArrowLeft") {
|
|
1044
1362
|
event.preventDefault();
|
|
1363
|
+
event.stopPropagation();
|
|
1045
1364
|
moveSelection(-1, 0);
|
|
1046
1365
|
}
|
|
1047
1366
|
else if (event.key === "ArrowRight") {
|
|
1048
1367
|
event.preventDefault();
|
|
1368
|
+
event.stopPropagation();
|
|
1049
1369
|
moveSelection(1, 0);
|
|
1050
1370
|
}
|
|
1051
1371
|
else if (event.key === "ArrowUp") {
|
|
1052
1372
|
event.preventDefault();
|
|
1373
|
+
event.stopPropagation();
|
|
1053
1374
|
moveSelection(0, -1);
|
|
1054
1375
|
}
|
|
1055
1376
|
else if (event.key === "ArrowDown") {
|
|
1056
1377
|
event.preventDefault();
|
|
1378
|
+
event.stopPropagation();
|
|
1057
1379
|
moveSelection(0, 1);
|
|
1058
1380
|
}
|
|
1059
1381
|
};
|
|
1060
1382
|
canvas.addEventListener("pointerdown", (event) => {
|
|
1383
|
+
if (event.pointerType !== "touch" && event.button !== 0)
|
|
1384
|
+
return;
|
|
1061
1385
|
if (event.pointerType === "touch") {
|
|
1062
1386
|
activeTouchPointers.set(event.pointerId, {
|
|
1063
1387
|
x: event.clientX,
|
|
@@ -1065,6 +1389,7 @@ export async function openFrameTouchUpEditor(options) {
|
|
|
1065
1389
|
});
|
|
1066
1390
|
if (activeTouchPointers.size >= 2) {
|
|
1067
1391
|
drawing = false;
|
|
1392
|
+
drawingPointerId = undefined;
|
|
1068
1393
|
selectionStart = undefined;
|
|
1069
1394
|
pinchStartDistance = touchPointerDistance();
|
|
1070
1395
|
pinchStartZoom = zoom;
|
|
@@ -1077,8 +1402,10 @@ export async function openFrameTouchUpEditor(options) {
|
|
|
1077
1402
|
pickColorAt(point);
|
|
1078
1403
|
return;
|
|
1079
1404
|
}
|
|
1405
|
+
floatingSelection = undefined;
|
|
1080
1406
|
pushUndo();
|
|
1081
1407
|
drawing = true;
|
|
1408
|
+
drawingPointerId = event.pointerId;
|
|
1082
1409
|
canvas.setPointerCapture(event.pointerId);
|
|
1083
1410
|
if (tool === "select") {
|
|
1084
1411
|
selectionStart = point;
|
|
@@ -1089,6 +1416,7 @@ export async function openFrameTouchUpEditor(options) {
|
|
|
1089
1416
|
if (tool === "fill") {
|
|
1090
1417
|
fillAt(point);
|
|
1091
1418
|
drawing = false;
|
|
1419
|
+
drawingPointerId = undefined;
|
|
1092
1420
|
setDirty(true);
|
|
1093
1421
|
redrawEditor();
|
|
1094
1422
|
return;
|
|
@@ -1111,8 +1439,12 @@ export async function openFrameTouchUpEditor(options) {
|
|
|
1111
1439
|
return;
|
|
1112
1440
|
}
|
|
1113
1441
|
}
|
|
1114
|
-
if (!drawing)
|
|
1442
|
+
if (!drawing || drawingPointerId !== event.pointerId)
|
|
1443
|
+
return;
|
|
1444
|
+
if (event.pointerType !== "touch" && (event.buttons & 1) === 0) {
|
|
1445
|
+
finishPointer(event);
|
|
1115
1446
|
return;
|
|
1447
|
+
}
|
|
1116
1448
|
const point = canvasPoint(event);
|
|
1117
1449
|
if (tool === "select" && selectionStart) {
|
|
1118
1450
|
const x = Math.min(selectionStart.x, point.x);
|
|
@@ -1132,7 +1464,7 @@ export async function openFrameTouchUpEditor(options) {
|
|
|
1132
1464
|
redrawEditor();
|
|
1133
1465
|
}
|
|
1134
1466
|
});
|
|
1135
|
-
|
|
1467
|
+
function finishPointer(event) {
|
|
1136
1468
|
if (event.pointerType === "touch") {
|
|
1137
1469
|
activeTouchPointers.delete(event.pointerId);
|
|
1138
1470
|
if (activeTouchPointers.size < 2) {
|
|
@@ -1143,15 +1475,29 @@ export async function openFrameTouchUpEditor(options) {
|
|
|
1143
1475
|
pinchStartZoom = zoom;
|
|
1144
1476
|
}
|
|
1145
1477
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1478
|
+
if (drawingPointerId === event.pointerId) {
|
|
1479
|
+
drawing = false;
|
|
1480
|
+
drawingPointerId = undefined;
|
|
1481
|
+
selectionStart = undefined;
|
|
1482
|
+
}
|
|
1148
1483
|
if (canvas.hasPointerCapture(event.pointerId)) {
|
|
1149
1484
|
canvas.releasePointerCapture(event.pointerId);
|
|
1150
1485
|
}
|
|
1151
1486
|
redrawEditor();
|
|
1152
|
-
}
|
|
1487
|
+
}
|
|
1488
|
+
function cancelDrawing() {
|
|
1489
|
+
drawing = false;
|
|
1490
|
+
drawingPointerId = undefined;
|
|
1491
|
+
selectionStart = undefined;
|
|
1492
|
+
activeTouchPointers.clear();
|
|
1493
|
+
pinchStartDistance = undefined;
|
|
1494
|
+
}
|
|
1153
1495
|
canvas.addEventListener("pointerup", finishPointer);
|
|
1154
1496
|
canvas.addEventListener("pointercancel", finishPointer);
|
|
1497
|
+
canvas.addEventListener("lostpointercapture", finishPointer);
|
|
1498
|
+
window.addEventListener("pointerup", finishPointer, true);
|
|
1499
|
+
window.addEventListener("pointercancel", finishPointer, true);
|
|
1500
|
+
window.addEventListener("blur", cancelDrawing);
|
|
1155
1501
|
canvasWrap.addEventListener("wheel", (event) => {
|
|
1156
1502
|
if (!event.ctrlKey && !event.metaKey)
|
|
1157
1503
|
return;
|
|
@@ -1162,6 +1508,15 @@ export async function openFrameTouchUpEditor(options) {
|
|
|
1162
1508
|
clientY: event.clientY
|
|
1163
1509
|
});
|
|
1164
1510
|
}, { passive: false });
|
|
1511
|
+
rotateSelectionButton.addEventListener("pointerdown", (event) => {
|
|
1512
|
+
event.preventDefault();
|
|
1513
|
+
event.stopPropagation();
|
|
1514
|
+
});
|
|
1515
|
+
rotateSelectionButton.addEventListener("click", (event) => {
|
|
1516
|
+
event.preventDefault();
|
|
1517
|
+
event.stopPropagation();
|
|
1518
|
+
rotateSelection();
|
|
1519
|
+
});
|
|
1165
1520
|
closeButton.addEventListener("click", requestClose);
|
|
1166
1521
|
zoomOutButton.addEventListener("click", () => {
|
|
1167
1522
|
setZoom(zoom - 1);
|
|
@@ -1170,54 +1525,45 @@ export async function openFrameTouchUpEditor(options) {
|
|
|
1170
1525
|
setZoom(zoom + 1);
|
|
1171
1526
|
});
|
|
1172
1527
|
brushButton.addEventListener("click", () => {
|
|
1173
|
-
|
|
1174
|
-
updateToolButtons();
|
|
1528
|
+
selectTool("brush");
|
|
1175
1529
|
});
|
|
1176
1530
|
brushMenuButton.addEventListener("click", () => {
|
|
1177
1531
|
brushMenu.hidden = !brushMenu.hidden;
|
|
1178
1532
|
});
|
|
1179
1533
|
brushSizeInput.addEventListener("input", () => {
|
|
1180
1534
|
brushSizeValue.textContent = brushSizeInput.value;
|
|
1535
|
+
frameTouchUpPreferences.brushSize = brushSize();
|
|
1536
|
+
});
|
|
1537
|
+
antiAliasInput.addEventListener("change", () => {
|
|
1538
|
+
frameTouchUpPreferences.antiAlias = antiAliasInput.checked;
|
|
1539
|
+
});
|
|
1540
|
+
colorInput.addEventListener("input", () => {
|
|
1541
|
+
frameTouchUpPreferences.color = colorInput.value;
|
|
1542
|
+
});
|
|
1543
|
+
alphaInput.addEventListener("input", () => {
|
|
1544
|
+
frameTouchUpPreferences.alpha = clamp(Math.round(Number(alphaInput.value)), 0, 255);
|
|
1181
1545
|
});
|
|
1182
1546
|
document.addEventListener("pointerdown", closeBrushMenuOnOutsidePointer);
|
|
1183
1547
|
eraserButton.addEventListener("click", () => {
|
|
1184
|
-
|
|
1185
|
-
updateToolButtons();
|
|
1548
|
+
selectTool("eraser");
|
|
1186
1549
|
});
|
|
1187
1550
|
pickerButton.addEventListener("click", () => {
|
|
1188
|
-
|
|
1189
|
-
updateToolButtons();
|
|
1551
|
+
selectTool("picker");
|
|
1190
1552
|
});
|
|
1191
1553
|
selectButton.addEventListener("click", () => {
|
|
1192
|
-
|
|
1193
|
-
updateToolButtons();
|
|
1554
|
+
selectTool("select");
|
|
1194
1555
|
});
|
|
1195
1556
|
fillButton.addEventListener("click", () => {
|
|
1196
|
-
|
|
1197
|
-
updateToolButtons();
|
|
1198
|
-
});
|
|
1199
|
-
undoButton.addEventListener("click", () => {
|
|
1200
|
-
const previous = undoStack.pop();
|
|
1201
|
-
if (!previous)
|
|
1202
|
-
return;
|
|
1203
|
-
redoStack.push(snapshot());
|
|
1204
|
-
restore(previous);
|
|
1205
|
-
updateUndoRedo();
|
|
1206
|
-
});
|
|
1207
|
-
redoButton.addEventListener("click", () => {
|
|
1208
|
-
const next = redoStack.pop();
|
|
1209
|
-
if (!next)
|
|
1210
|
-
return;
|
|
1211
|
-
undoStack.push(snapshot());
|
|
1212
|
-
restore(next);
|
|
1213
|
-
updateUndoRedo();
|
|
1557
|
+
selectTool("fill");
|
|
1214
1558
|
});
|
|
1559
|
+
undoButton.addEventListener("click", undo);
|
|
1560
|
+
redoButton.addEventListener("click", redo);
|
|
1215
1561
|
saveButton.addEventListener("click", async () => {
|
|
1216
1562
|
await options.onSave(canvas.toDataURL("image/png"));
|
|
1217
1563
|
setDirty(false);
|
|
1218
1564
|
close();
|
|
1219
1565
|
});
|
|
1220
|
-
window.addEventListener("keydown", keyHandler);
|
|
1566
|
+
window.addEventListener("keydown", keyHandler, true);
|
|
1221
1567
|
updateZoom();
|
|
1222
1568
|
updateToolButtons();
|
|
1223
1569
|
updateUndoRedo();
|
|
@@ -1358,6 +1704,14 @@ export function renderAudioPlayer(options) {
|
|
|
1358
1704
|
state.frame = window.requestAnimationFrame(tick);
|
|
1359
1705
|
}
|
|
1360
1706
|
};
|
|
1707
|
+
const handleEnded = () => {
|
|
1708
|
+
if (!options.playback?.loop || state.disposed) {
|
|
1709
|
+
sync();
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
audio.currentTime = state.trimStart;
|
|
1713
|
+
void audio.play().then(() => tick()).catch(() => sync());
|
|
1714
|
+
};
|
|
1361
1715
|
playButton.addEventListener("click", async () => {
|
|
1362
1716
|
if (audio.paused) {
|
|
1363
1717
|
pauseSiblingAudioPlayers(options.container);
|
|
@@ -1392,12 +1746,15 @@ export function renderAudioPlayer(options) {
|
|
|
1392
1746
|
state.trimStart = clamp(options.playback?.trimStartSeconds ?? 0, 0, state.duration);
|
|
1393
1747
|
state.trimEnd = clamp(options.playback?.trimEndSeconds ?? state.duration, state.trimStart, state.duration);
|
|
1394
1748
|
trimLabel.hidden = state.trimStart === 0 && state.trimEnd === state.duration;
|
|
1395
|
-
trimLabel.textContent = `Trim ${formatAudioTime(state.trimStart)} – ${formatAudioTime(state.trimEnd)}`;
|
|
1749
|
+
trimLabel.textContent = `Trim ${formatAudioTime(state.trimStart, true)} – ${formatAudioTime(state.trimEnd, true)}`;
|
|
1750
|
+
audio.loop = Boolean(options.playback?.loop) &&
|
|
1751
|
+
state.trimStart === 0 &&
|
|
1752
|
+
state.trimEnd >= state.duration - 0.05;
|
|
1396
1753
|
audio.currentTime = state.trimStart;
|
|
1397
1754
|
sync();
|
|
1398
1755
|
});
|
|
1399
1756
|
audio.addEventListener("timeupdate", sync);
|
|
1400
|
-
audio.addEventListener("ended",
|
|
1757
|
+
audio.addEventListener("ended", handleEnded);
|
|
1401
1758
|
draw();
|
|
1402
1759
|
void audioWaveformPeaks(options.src).then((peaks) => {
|
|
1403
1760
|
if (state.disposed)
|
|
@@ -1515,13 +1872,17 @@ export function progressForAudio(audio) {
|
|
|
1515
1872
|
export function clamp(value, min, max) {
|
|
1516
1873
|
return Math.min(max, Math.max(min, value));
|
|
1517
1874
|
}
|
|
1518
|
-
export function formatAudioTime(value) {
|
|
1875
|
+
export function formatAudioTime(value, fractional = false) {
|
|
1519
1876
|
if (!Number.isFinite(value) || value < 0) {
|
|
1520
1877
|
return "0:00";
|
|
1521
1878
|
}
|
|
1522
|
-
const
|
|
1523
|
-
const
|
|
1524
|
-
|
|
1879
|
+
const precision = fractional ? 2 : 0;
|
|
1880
|
+
const multiplier = 10 ** precision;
|
|
1881
|
+
const roundedValue = Math.round(value * multiplier) / multiplier;
|
|
1882
|
+
const minutes = Math.floor(roundedValue / 60);
|
|
1883
|
+
const seconds = roundedValue % 60;
|
|
1884
|
+
const formattedSeconds = seconds.toFixed(precision).padStart(fractional ? 5 : 2, "0");
|
|
1885
|
+
return `${minutes}:${formattedSeconds}`;
|
|
1525
1886
|
}
|
|
1526
1887
|
export function startSpritesheetPreview(options) {
|
|
1527
1888
|
const frameGrid = options.asset.frameGrid;
|
|
@@ -1657,9 +2018,12 @@ export async function openStyleGuideEditor(options) {
|
|
|
1657
2018
|
const promoteButton = document.createElement("button");
|
|
1658
2019
|
promoteButton.type = "button";
|
|
1659
2020
|
promoteButton.textContent = "Promote style";
|
|
2021
|
+
const regenerateAllButton = document.createElement("button");
|
|
2022
|
+
regenerateAllButton.type = "button";
|
|
2023
|
+
regenerateAllButton.textContent = "Regenerate all...";
|
|
1660
2024
|
const actions = document.createElement("div");
|
|
1661
2025
|
actions.className = "ai-game-assets-designer__modal-actions";
|
|
1662
|
-
actions.append(cancelButton, promoteButton, confirmButton);
|
|
2026
|
+
actions.append(cancelButton, promoteButton, regenerateAllButton, confirmButton);
|
|
1663
2027
|
const draft = () => ({
|
|
1664
2028
|
prompt: promptInput.value.trim(),
|
|
1665
2029
|
images: images.map((image) => ({ ...image }))
|
|
@@ -1685,6 +2049,12 @@ export async function openStyleGuideEditor(options) {
|
|
|
1685
2049
|
promoteButton.disabled = false;
|
|
1686
2050
|
}
|
|
1687
2051
|
});
|
|
2052
|
+
regenerateAllButton.addEventListener("click", () => {
|
|
2053
|
+
const current = draft();
|
|
2054
|
+
options.onConfirm(current);
|
|
2055
|
+
close();
|
|
2056
|
+
void options.onRegenerateAll(current);
|
|
2057
|
+
});
|
|
1688
2058
|
card.append(title, promptField, dropZone, uploadButton, fileInput, imageList, actions);
|
|
1689
2059
|
dialog.append(card);
|
|
1690
2060
|
options.root.append(dialog);
|
|
@@ -2364,11 +2734,33 @@ export function setFrameBackground(element, options) {
|
|
|
2364
2734
|
export function setFrameElementBackground(frameElement, options) {
|
|
2365
2735
|
const column = options.frame % options.frameGrid.columns;
|
|
2366
2736
|
const row = Math.floor(options.frame / options.frameGrid.columns);
|
|
2737
|
+
const margin = options.frameGrid.margin ?? 0;
|
|
2738
|
+
const spacing = options.frameGrid.spacing ?? 0;
|
|
2739
|
+
const frameWidth = options.frameGrid.frameWidth;
|
|
2740
|
+
const frameHeight = options.frameGrid.frameHeight;
|
|
2741
|
+
const scale = Math.min(options.displaySize.width / frameWidth, options.displaySize.height / frameHeight);
|
|
2742
|
+
const sheetWidth = (margin * 2) +
|
|
2743
|
+
(options.frameGrid.columns * frameWidth) +
|
|
2744
|
+
(Math.max(0, options.frameGrid.columns - 1) * spacing);
|
|
2745
|
+
const sheetHeight = (margin * 2) +
|
|
2746
|
+
(options.frameGrid.rows * frameHeight) +
|
|
2747
|
+
(Math.max(0, options.frameGrid.rows - 1) * spacing);
|
|
2748
|
+
const sourceX = margin + (column * (frameWidth + spacing));
|
|
2749
|
+
const sourceY = margin + (row * (frameHeight + spacing));
|
|
2750
|
+
const displayFrameWidth = frameWidth * scale;
|
|
2751
|
+
const displayFrameHeight = frameHeight * scale;
|
|
2752
|
+
const frameOffsetX = (options.displaySize.width - displayFrameWidth) / 2;
|
|
2753
|
+
const frameOffsetY = (options.displaySize.height - displayFrameHeight) / 2;
|
|
2754
|
+
frameElement.style.inset = "auto";
|
|
2755
|
+
frameElement.style.left = `${frameOffsetX}px`;
|
|
2756
|
+
frameElement.style.top = `${frameOffsetY}px`;
|
|
2757
|
+
frameElement.style.width = `${displayFrameWidth}px`;
|
|
2758
|
+
frameElement.style.height = `${displayFrameHeight}px`;
|
|
2367
2759
|
frameElement.style.backgroundImage = `url("${cssUrl(options.src)}")`;
|
|
2368
2760
|
frameElement.style.backgroundSize =
|
|
2369
|
-
`${
|
|
2761
|
+
`${sheetWidth * scale}px ${sheetHeight * scale}px`;
|
|
2370
2762
|
frameElement.style.backgroundPosition =
|
|
2371
|
-
`${-(
|
|
2763
|
+
`${-(sourceX * scale)}px ${-(sourceY * scale)}px`;
|
|
2372
2764
|
}
|
|
2373
2765
|
export function ensureFramePreviewElement(element) {
|
|
2374
2766
|
const existing = element.querySelector(":scope > .ai-game-assets-designer__frame-image");
|
|
@@ -2380,10 +2772,21 @@ export function ensureFramePreviewElement(element) {
|
|
|
2380
2772
|
return frameElement;
|
|
2381
2773
|
}
|
|
2382
2774
|
export function generationOverridesFromInputs(elements, asset, format) {
|
|
2775
|
+
const tileset = designerTilesetMetadata(asset);
|
|
2383
2776
|
const dimensions = {
|
|
2384
|
-
width: positiveIntegerInput(elements.widthInput, asset.frameGrid?.frameWidth ?? asset.dimensions?.width ?? 1),
|
|
2385
|
-
height: positiveIntegerInput(elements.heightInput, asset.frameGrid?.frameHeight ?? asset.dimensions?.height ?? 1)
|
|
2777
|
+
width: positiveIntegerInput(elements.widthInput, asset.frameGrid?.frameWidth ?? tileset?.tileWidth ?? asset.dimensions?.width ?? 1),
|
|
2778
|
+
height: positiveIntegerInput(elements.heightInput, asset.frameGrid?.frameHeight ?? tileset?.tileHeight ?? asset.dimensions?.height ?? 1)
|
|
2386
2779
|
};
|
|
2780
|
+
if (tileset) {
|
|
2781
|
+
return {
|
|
2782
|
+
tileset: {
|
|
2783
|
+
tileWidth: dimensions.width,
|
|
2784
|
+
tileHeight: dimensions.height,
|
|
2785
|
+
tileCount: positiveIntegerInput(elements.frameCountInput, tileset.tileCount ?? tileset.columns * tileset.rows)
|
|
2786
|
+
},
|
|
2787
|
+
settings: { format }
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2387
2790
|
if (!asset.frameGrid) {
|
|
2388
2791
|
return {
|
|
2389
2792
|
dimensions,
|
|
@@ -2452,6 +2855,9 @@ export function isAudioAsset(asset) {
|
|
|
2452
2855
|
asset?.kind === "voice" ||
|
|
2453
2856
|
asset?.kind === "voice-line";
|
|
2454
2857
|
}
|
|
2858
|
+
function designerTilesetMetadata(asset) {
|
|
2859
|
+
return asset?.tileset;
|
|
2860
|
+
}
|
|
2455
2861
|
export function isVoiceAsset(asset) {
|
|
2456
2862
|
return asset?.kind === "voice" || asset?.kind === "voice-line";
|
|
2457
2863
|
}
|
|
@@ -2460,6 +2866,7 @@ export function assetWithGeneratedGeometry(asset, option, options = {}) {
|
|
|
2460
2866
|
...asset,
|
|
2461
2867
|
dimensions: option.dimensions ?? asset.dimensions,
|
|
2462
2868
|
frameGrid: option.frameGrid ?? asset.frameGrid,
|
|
2869
|
+
tileset: option.tileset ?? asset.tileset,
|
|
2463
2870
|
animations: option.animations ??
|
|
2464
2871
|
(options.inheritAnimations
|
|
2465
2872
|
? asset.animations
|
|
@@ -2481,7 +2888,11 @@ export function positiveIntegerInput(input, fallback) {
|
|
|
2481
2888
|
const value = Number(input.value);
|
|
2482
2889
|
if (!Number.isFinite(value))
|
|
2483
2890
|
return fallback;
|
|
2484
|
-
|
|
2891
|
+
const normalized = Math.max(1, Math.floor(value));
|
|
2892
|
+
const maximum = Number(input.max);
|
|
2893
|
+
return input.max && Number.isFinite(maximum)
|
|
2894
|
+
? Math.min(normalized, Math.max(1, Math.floor(maximum)))
|
|
2895
|
+
: normalized;
|
|
2485
2896
|
}
|
|
2486
2897
|
export function positiveNumberInput(input, fallback) {
|
|
2487
2898
|
const value = Number(input.value);
|
|
@@ -2612,7 +3023,7 @@ export async function imageSourceToDataUrl(src) {
|
|
|
2612
3023
|
return src;
|
|
2613
3024
|
const response = await fetch(src);
|
|
2614
3025
|
if (!response.ok) {
|
|
2615
|
-
throw new Error(`Could not load current
|
|
3026
|
+
throw new Error(`Could not load current asset image (${response.status}).`);
|
|
2616
3027
|
}
|
|
2617
3028
|
const responseBlob = await response.blob();
|
|
2618
3029
|
const blob = responseBlob.type === "application/octet-stream"
|
|
@@ -2625,11 +3036,11 @@ export async function imageSourceToDataUrl(src) {
|
|
|
2625
3036
|
resolve(reader.result);
|
|
2626
3037
|
}
|
|
2627
3038
|
else {
|
|
2628
|
-
reject(new Error("Could not convert current
|
|
3039
|
+
reject(new Error("Could not convert current asset image to a data URL."));
|
|
2629
3040
|
}
|
|
2630
3041
|
});
|
|
2631
3042
|
reader.addEventListener("error", () => {
|
|
2632
|
-
reject(reader.error ?? new Error("Could not read current
|
|
3043
|
+
reject(reader.error ?? new Error("Could not read current asset image."));
|
|
2633
3044
|
});
|
|
2634
3045
|
reader.readAsDataURL(blob);
|
|
2635
3046
|
});
|
|
@@ -2835,6 +3246,36 @@ export function ensureDesignerStyles() {
|
|
|
2835
3246
|
font: inherit;
|
|
2836
3247
|
}
|
|
2837
3248
|
.ai-game-assets-designer__field textarea { resize: vertical; }
|
|
3249
|
+
.ai-game-assets-designer__tile-prompts {
|
|
3250
|
+
display: grid;
|
|
3251
|
+
gap: 7px;
|
|
3252
|
+
margin-bottom: 12px;
|
|
3253
|
+
padding: 10px;
|
|
3254
|
+
border: 1px solid #303949;
|
|
3255
|
+
border-radius: 8px;
|
|
3256
|
+
background: rgba(11, 15, 22, 0.58);
|
|
3257
|
+
}
|
|
3258
|
+
.ai-game-assets-designer__tile-prompts[hidden] { display: none; }
|
|
3259
|
+
.ai-game-assets-designer__tile-prompts-title {
|
|
3260
|
+
color: #e5edf9;
|
|
3261
|
+
font-size: 13px;
|
|
3262
|
+
font-weight: 700;
|
|
3263
|
+
}
|
|
3264
|
+
.ai-game-assets-designer__tile-prompts-hint {
|
|
3265
|
+
color: #8f9cb5;
|
|
3266
|
+
font-size: 11px;
|
|
3267
|
+
line-height: 1.35;
|
|
3268
|
+
}
|
|
3269
|
+
.ai-game-assets-designer__tile-prompts-list {
|
|
3270
|
+
display: grid;
|
|
3271
|
+
gap: 8px;
|
|
3272
|
+
}
|
|
3273
|
+
.ai-game-assets-designer__tile-prompts-list .ai-game-assets-designer__field {
|
|
3274
|
+
margin-bottom: 0;
|
|
3275
|
+
}
|
|
3276
|
+
.ai-game-assets-designer__tile-prompts-list textarea {
|
|
3277
|
+
min-height: 62px;
|
|
3278
|
+
}
|
|
2838
3279
|
.ai-game-assets-designer__target-variant {
|
|
2839
3280
|
margin: -4px 0 12px;
|
|
2840
3281
|
padding: 7px 9px;
|
|
@@ -3059,6 +3500,17 @@ export function ensureDesignerStyles() {
|
|
|
3059
3500
|
gap: 8px;
|
|
3060
3501
|
margin-top: 10px;
|
|
3061
3502
|
}
|
|
3503
|
+
.ai-game-assets-designer__tileset-tile-prompt {
|
|
3504
|
+
margin-top: 10px;
|
|
3505
|
+
padding: 9px 10px;
|
|
3506
|
+
border: 1px solid #354058;
|
|
3507
|
+
border-radius: 6px;
|
|
3508
|
+
background: rgba(37, 49, 73, 0.48);
|
|
3509
|
+
color: #d8e4f7;
|
|
3510
|
+
font-size: 12px;
|
|
3511
|
+
line-height: 1.45;
|
|
3512
|
+
white-space: pre-wrap;
|
|
3513
|
+
}
|
|
3062
3514
|
.ai-game-assets-designer__options.is-audio {
|
|
3063
3515
|
grid-template-columns: 1fr;
|
|
3064
3516
|
}
|
|
@@ -3085,6 +3537,7 @@ export function ensureDesignerStyles() {
|
|
|
3085
3537
|
background: transparent;
|
|
3086
3538
|
display: grid;
|
|
3087
3539
|
place-items: center;
|
|
3540
|
+
overflow: hidden;
|
|
3088
3541
|
cursor: pointer;
|
|
3089
3542
|
color: #dbeafe;
|
|
3090
3543
|
font: inherit;
|
|
@@ -3105,12 +3558,18 @@ export function ensureDesignerStyles() {
|
|
|
3105
3558
|
object-fit: contain;
|
|
3106
3559
|
image-rendering: pixelated;
|
|
3107
3560
|
}
|
|
3561
|
+
.ai-game-assets-designer__option-animation {
|
|
3562
|
+
width: 68px;
|
|
3563
|
+
height: 68px;
|
|
3564
|
+
max-width: 100%;
|
|
3565
|
+
}
|
|
3108
3566
|
.ai-game-assets-designer__option .ai-game-assets-designer__animate-button {
|
|
3109
3567
|
width: 100%;
|
|
3110
3568
|
}
|
|
3111
3569
|
.ai-game-assets-designer__modal {
|
|
3112
3570
|
position: fixed;
|
|
3113
3571
|
inset: 0;
|
|
3572
|
+
z-index: 2147483647;
|
|
3114
3573
|
display: grid;
|
|
3115
3574
|
place-items: center;
|
|
3116
3575
|
padding: 18px;
|
|
@@ -3126,6 +3585,159 @@ export function ensureDesignerStyles() {
|
|
|
3126
3585
|
box-shadow: 0 22px 70px rgba(0, 0, 0, 0.55);
|
|
3127
3586
|
padding: 14px;
|
|
3128
3587
|
}
|
|
3588
|
+
.ai-game-assets-designer__tileset-mixer-card {
|
|
3589
|
+
width: min(980px, calc(100vw - 36px));
|
|
3590
|
+
}
|
|
3591
|
+
.ai-game-assets-designer__tileset-editor-card {
|
|
3592
|
+
width: min(720px, calc(100vw - 36px));
|
|
3593
|
+
}
|
|
3594
|
+
.ai-game-assets-designer__tileset-editor-stage {
|
|
3595
|
+
border: 1px solid #303a49;
|
|
3596
|
+
border-radius: 8px;
|
|
3597
|
+
}
|
|
3598
|
+
.ai-game-assets-designer__tileset-editor-strip {
|
|
3599
|
+
margin-top: 8px;
|
|
3600
|
+
}
|
|
3601
|
+
.ai-game-assets-designer__tileset-transform-fields {
|
|
3602
|
+
margin-bottom: 6px;
|
|
3603
|
+
}
|
|
3604
|
+
.ai-game-assets-designer__tileset-transform-hint {
|
|
3605
|
+
margin: 0 0 10px;
|
|
3606
|
+
color: #aeb8c8;
|
|
3607
|
+
font-size: 12px;
|
|
3608
|
+
line-height: 1.4;
|
|
3609
|
+
}
|
|
3610
|
+
.ai-game-assets-designer__tileset-transform-thumb {
|
|
3611
|
+
width: 48px;
|
|
3612
|
+
height: 48px;
|
|
3613
|
+
display: grid;
|
|
3614
|
+
place-items: center;
|
|
3615
|
+
}
|
|
3616
|
+
.ai-game-assets-designer__tileset-transform-thumb-viewport {
|
|
3617
|
+
position: relative;
|
|
3618
|
+
display: block;
|
|
3619
|
+
overflow: hidden;
|
|
3620
|
+
border-radius: 3px;
|
|
3621
|
+
pointer-events: none;
|
|
3622
|
+
}
|
|
3623
|
+
@media (max-width: 520px) {
|
|
3624
|
+
.ai-game-assets-designer__tileset-transform-fields {
|
|
3625
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
.ai-game-assets-designer__tileset-regenerate-card {
|
|
3629
|
+
width: min(620px, calc(100vw - 36px));
|
|
3630
|
+
}
|
|
3631
|
+
.ai-game-assets-designer__tileset-regenerate-card .ai-game-assets-designer__tileset-tile-prompt {
|
|
3632
|
+
margin: 0 0 12px;
|
|
3633
|
+
}
|
|
3634
|
+
.ai-game-assets-designer__tileset-mixer-hint {
|
|
3635
|
+
margin: -5px 0 12px;
|
|
3636
|
+
color: #aeb8c8;
|
|
3637
|
+
font-size: 12px;
|
|
3638
|
+
line-height: 1.4;
|
|
3639
|
+
}
|
|
3640
|
+
.ai-game-assets-designer__tileset-mixer-body {
|
|
3641
|
+
display: grid;
|
|
3642
|
+
grid-template-columns: minmax(160px, 220px) minmax(0, 1fr);
|
|
3643
|
+
gap: 14px;
|
|
3644
|
+
min-height: 300px;
|
|
3645
|
+
}
|
|
3646
|
+
.ai-game-assets-designer__tileset-navigator-panel,
|
|
3647
|
+
.ai-game-assets-designer__tileset-preview-panel {
|
|
3648
|
+
min-width: 0;
|
|
3649
|
+
border: 1px solid #303a49;
|
|
3650
|
+
border-radius: 8px;
|
|
3651
|
+
background: #0f1218;
|
|
3652
|
+
padding: 10px;
|
|
3653
|
+
}
|
|
3654
|
+
.ai-game-assets-designer__tileset-section-title {
|
|
3655
|
+
min-height: 20px;
|
|
3656
|
+
margin-bottom: 8px;
|
|
3657
|
+
color: #dbeafe;
|
|
3658
|
+
font-size: 12px;
|
|
3659
|
+
font-weight: 700;
|
|
3660
|
+
}
|
|
3661
|
+
.ai-game-assets-designer__tileset-navigator {
|
|
3662
|
+
display: grid;
|
|
3663
|
+
grid-template-columns: repeat(auto-fill, minmax(56px, 1fr));
|
|
3664
|
+
gap: 7px;
|
|
3665
|
+
max-height: min(58vh, 540px);
|
|
3666
|
+
overflow: auto;
|
|
3667
|
+
padding: 2px;
|
|
3668
|
+
}
|
|
3669
|
+
.ai-game-assets-designer__tileset-tile {
|
|
3670
|
+
position: relative;
|
|
3671
|
+
overflow: hidden;
|
|
3672
|
+
border: 2px solid #384251;
|
|
3673
|
+
border-radius: 6px;
|
|
3674
|
+
background: #111827;
|
|
3675
|
+
cursor: pointer;
|
|
3676
|
+
}
|
|
3677
|
+
.ai-game-assets-designer__tileset-tile::after {
|
|
3678
|
+
content: attr(data-source);
|
|
3679
|
+
position: absolute;
|
|
3680
|
+
right: 2px;
|
|
3681
|
+
bottom: 2px;
|
|
3682
|
+
z-index: 3;
|
|
3683
|
+
border-radius: 999px;
|
|
3684
|
+
background: rgba(8, 12, 20, 0.88);
|
|
3685
|
+
color: #9ca8ba;
|
|
3686
|
+
padding: 1px 4px;
|
|
3687
|
+
font-size: 9px;
|
|
3688
|
+
line-height: 1.4;
|
|
3689
|
+
}
|
|
3690
|
+
.ai-game-assets-designer__tileset-tile.is-selected {
|
|
3691
|
+
border-color: #6ed3ff;
|
|
3692
|
+
box-shadow: 0 0 0 1px rgba(110, 211, 255, 0.35);
|
|
3693
|
+
}
|
|
3694
|
+
.ai-game-assets-designer__tileset-tile.is-mixed::after {
|
|
3695
|
+
background: #155e75;
|
|
3696
|
+
color: #ecfeff;
|
|
3697
|
+
}
|
|
3698
|
+
.ai-game-assets-designer__tileset-choices {
|
|
3699
|
+
display: grid;
|
|
3700
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
3701
|
+
gap: 10px;
|
|
3702
|
+
align-items: start;
|
|
3703
|
+
width: 100%;
|
|
3704
|
+
min-width: 0;
|
|
3705
|
+
}
|
|
3706
|
+
.ai-game-assets-designer__tileset-choice {
|
|
3707
|
+
display: grid;
|
|
3708
|
+
justify-items: center;
|
|
3709
|
+
gap: 7px;
|
|
3710
|
+
min-width: 0;
|
|
3711
|
+
max-width: 100%;
|
|
3712
|
+
overflow: hidden;
|
|
3713
|
+
border: 2px solid #384251;
|
|
3714
|
+
border-radius: 8px;
|
|
3715
|
+
background: #151b25;
|
|
3716
|
+
color: #dbeafe;
|
|
3717
|
+
padding: 9px;
|
|
3718
|
+
font: inherit;
|
|
3719
|
+
font-size: 12px;
|
|
3720
|
+
cursor: pointer;
|
|
3721
|
+
}
|
|
3722
|
+
.ai-game-assets-designer__tileset-choice.is-selected {
|
|
3723
|
+
border-color: #6ed3ff;
|
|
3724
|
+
background: #16283a;
|
|
3725
|
+
}
|
|
3726
|
+
.ai-game-assets-designer__tileset-choice-preview {
|
|
3727
|
+
position: relative;
|
|
3728
|
+
overflow: hidden;
|
|
3729
|
+
max-width: 100%;
|
|
3730
|
+
border-radius: 6px;
|
|
3731
|
+
background: #0a0d12;
|
|
3732
|
+
}
|
|
3733
|
+
@media (max-width: 760px) {
|
|
3734
|
+
.ai-game-assets-designer__tileset-mixer-body {
|
|
3735
|
+
grid-template-columns: 1fr;
|
|
3736
|
+
}
|
|
3737
|
+
.ai-game-assets-designer__tileset-navigator {
|
|
3738
|
+
max-height: 180px;
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3129
3741
|
.ai-game-assets-designer__modal-title {
|
|
3130
3742
|
margin-bottom: 12px;
|
|
3131
3743
|
font-weight: 700;
|
|
@@ -3149,6 +3761,9 @@ export function ensureDesignerStyles() {
|
|
|
3149
3761
|
.ai-game-assets-designer__modal-feedback[data-kind="error"] {
|
|
3150
3762
|
color: #ffb4b4;
|
|
3151
3763
|
}
|
|
3764
|
+
.ai-game-assets-designer__modal-feedback[data-kind="success"] {
|
|
3765
|
+
color: #86efac;
|
|
3766
|
+
}
|
|
3152
3767
|
.ai-game-assets-designer__version-list {
|
|
3153
3768
|
display: grid;
|
|
3154
3769
|
gap: 10px;
|
|
@@ -3242,16 +3857,29 @@ export function ensureDesignerStyles() {
|
|
|
3242
3857
|
}
|
|
3243
3858
|
.ai-game-assets-designer__frame-thumb {
|
|
3244
3859
|
flex: 0 0 auto;
|
|
3245
|
-
border:
|
|
3860
|
+
border: 0;
|
|
3246
3861
|
border-radius: 6px;
|
|
3247
3862
|
background-color: #0f1218;
|
|
3248
3863
|
position: relative;
|
|
3249
3864
|
overflow: hidden;
|
|
3250
3865
|
cursor: pointer;
|
|
3251
3866
|
}
|
|
3252
|
-
.ai-game-assets-designer__frame-thumb
|
|
3867
|
+
.ai-game-assets-designer__frame-thumb::after {
|
|
3868
|
+
content: "";
|
|
3869
|
+
position: absolute;
|
|
3870
|
+
inset: 0;
|
|
3871
|
+
z-index: 2;
|
|
3872
|
+
border: 2px solid #384251;
|
|
3873
|
+
border-radius: inherit;
|
|
3874
|
+
pointer-events: none;
|
|
3875
|
+
box-sizing: border-box;
|
|
3876
|
+
}
|
|
3877
|
+
.ai-game-assets-designer__frame-thumb.is-selected::after {
|
|
3253
3878
|
border-color: #6ed3ff;
|
|
3254
3879
|
}
|
|
3880
|
+
.ai-game-assets-designer__frame-thumb.is-previewing::after {
|
|
3881
|
+
box-shadow: inset 0 -3px 0 #facc15;
|
|
3882
|
+
}
|
|
3255
3883
|
.ai-game-assets-designer__frame-fields {
|
|
3256
3884
|
display: grid;
|
|
3257
3885
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
@@ -3401,7 +4029,7 @@ export function ensureDesignerStyles() {
|
|
|
3401
4029
|
.ai-game-assets-designer__touchup {
|
|
3402
4030
|
position: fixed;
|
|
3403
4031
|
inset: 0;
|
|
3404
|
-
z-index:
|
|
4032
|
+
z-index: 2147483647;
|
|
3405
4033
|
display: grid;
|
|
3406
4034
|
grid-template-rows: auto auto minmax(0, 1fr);
|
|
3407
4035
|
gap: 10px;
|
|
@@ -3587,6 +4215,30 @@ export function ensureDesignerStyles() {
|
|
|
3587
4215
|
box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.9);
|
|
3588
4216
|
transform: translate(24px, 24px);
|
|
3589
4217
|
}
|
|
4218
|
+
.ai-game-assets-designer__touchup-selection-rotate {
|
|
4219
|
+
position: absolute;
|
|
4220
|
+
top: 2px;
|
|
4221
|
+
right: 2px;
|
|
4222
|
+
z-index: 2;
|
|
4223
|
+
display: grid;
|
|
4224
|
+
place-items: center;
|
|
4225
|
+
min-width: 0;
|
|
4226
|
+
min-height: 0;
|
|
4227
|
+
border: 1px solid #dbeafe;
|
|
4228
|
+
border-radius: 999px;
|
|
4229
|
+
background: rgba(15, 23, 42, 0.94);
|
|
4230
|
+
color: #f8fafc;
|
|
4231
|
+
padding: 0;
|
|
4232
|
+
font: 700 11px/1 ui-sans-serif, system-ui, sans-serif;
|
|
4233
|
+
cursor: pointer;
|
|
4234
|
+
pointer-events: auto;
|
|
4235
|
+
}
|
|
4236
|
+
.ai-game-assets-designer__touchup-selection-rotate:hover,
|
|
4237
|
+
.ai-game-assets-designer__touchup-selection-rotate:focus-visible {
|
|
4238
|
+
border-color: #6ed3ff;
|
|
4239
|
+
color: #6ed3ff;
|
|
4240
|
+
outline: none;
|
|
4241
|
+
}
|
|
3590
4242
|
.ai-game-assets-designer__touchup-side {
|
|
3591
4243
|
min-height: 0;
|
|
3592
4244
|
display: grid;
|