@ai-game-assets/phaser 0.1.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 +4 -0
- package/dist/animations.d.ts.map +1 -0
- package/dist/animations.js +36 -0
- package/dist/animations.js.map +1 -0
- package/dist/debug-client.d.ts +109 -0
- package/dist/debug-client.d.ts.map +1 -0
- package/dist/debug-client.js +157 -0
- package/dist/debug-client.js.map +1 -0
- package/dist/designer-support.d.ts +271 -0
- package/dist/designer-support.d.ts.map +1 -0
- package/dist/designer-support.js +3341 -0
- package/dist/designer-support.js.map +1 -0
- package/dist/designer.d.ts +49 -0
- package/dist/designer.d.ts.map +1 -0
- package/dist/designer.js +592 -0
- package/dist/designer.js.map +1 -0
- package/dist/first-drafts.d.ts +36 -0
- package/dist/first-drafts.d.ts.map +1 -0
- package/dist/first-drafts.js +178 -0
- package/dist/first-drafts.js.map +1 -0
- package/dist/frame-transforms.d.ts +24 -0
- package/dist/frame-transforms.d.ts.map +1 -0
- package/dist/frame-transforms.js +33 -0
- package/dist/frame-transforms.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +3 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +9 -0
- package/dist/keys.js.map +1 -0
- package/dist/loader.d.ts +12 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +130 -0
- package/dist/loader.js.map +1 -0
- package/dist/phaser-types.d.ts +41 -0
- package/dist/phaser-types.d.ts.map +1 -0
- package/dist/phaser-types.js +2 -0
- package/dist/phaser-types.js.map +1 -0
- package/dist/placeholder.d.ts +3 -0
- package/dist/placeholder.d.ts.map +1 -0
- package/dist/placeholder.js +56 -0
- package/dist/placeholder.js.map +1 -0
- package/dist/runtime.d.ts +15 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +43 -0
- package/dist/runtime.js.map +1 -0
- package/package.json +30 -0
|
@@ -0,0 +1,3341 @@
|
|
|
1
|
+
const assetFormatOptions = [
|
|
2
|
+
{ value: "png", label: "PNG" },
|
|
3
|
+
{ value: "jpg", label: "JPEG" },
|
|
4
|
+
{ value: "webp", label: "WebP" },
|
|
5
|
+
{ value: "svg", label: "SVG" }
|
|
6
|
+
];
|
|
7
|
+
const audioFormatOptions = [
|
|
8
|
+
{ value: "mp3", label: "MP3" },
|
|
9
|
+
{ value: "wav", label: "WAV" },
|
|
10
|
+
{ value: "ogg", label: "OGG" },
|
|
11
|
+
{ value: "opus", label: "Opus" },
|
|
12
|
+
{ value: "pcm", label: "PCM" }
|
|
13
|
+
];
|
|
14
|
+
export function createDesignerElements(options, manifest, selectedAssetId) {
|
|
15
|
+
const root = document.createElement("div");
|
|
16
|
+
root.className = "ai-game-assets-designer";
|
|
17
|
+
root.dataset.open = "false";
|
|
18
|
+
const toggle = document.createElement("button");
|
|
19
|
+
toggle.type = "button";
|
|
20
|
+
toggle.className = "ai-game-assets-designer__toggle";
|
|
21
|
+
toggle.setAttribute("aria-label", "Toggle AI asset designer");
|
|
22
|
+
toggle.setAttribute("aria-expanded", "false");
|
|
23
|
+
toggle.textContent = "Assets";
|
|
24
|
+
const panel = document.createElement("div");
|
|
25
|
+
panel.className = "ai-game-assets-designer__panel";
|
|
26
|
+
const title = document.createElement("div");
|
|
27
|
+
title.className = "ai-game-assets-designer__title";
|
|
28
|
+
title.textContent = options.title ?? "AI Asset Designer";
|
|
29
|
+
const styleButton = document.createElement("button");
|
|
30
|
+
styleButton.type = "button";
|
|
31
|
+
styleButton.className = "ai-game-assets-designer__style-button";
|
|
32
|
+
styleButton.textContent = "Define style...";
|
|
33
|
+
const header = document.createElement("div");
|
|
34
|
+
header.className = "ai-game-assets-designer__header";
|
|
35
|
+
header.append(title, styleButton);
|
|
36
|
+
const assetSelect = document.createElement("select");
|
|
37
|
+
assetSelect.className = "ai-game-assets-designer__select";
|
|
38
|
+
assetSelect.hidden = true;
|
|
39
|
+
const assetBrowser = document.createElement("div");
|
|
40
|
+
assetBrowser.className = "ai-game-assets-designer__asset-browser";
|
|
41
|
+
for (const assetId of options.assetIds ?? Object.keys(manifest.assets)) {
|
|
42
|
+
const option = document.createElement("option");
|
|
43
|
+
option.value = assetId;
|
|
44
|
+
option.textContent = readableAssetName(assetId);
|
|
45
|
+
option.selected = assetId === selectedAssetId;
|
|
46
|
+
assetSelect.append(option);
|
|
47
|
+
}
|
|
48
|
+
const animationSelect = document.createElement("select");
|
|
49
|
+
animationSelect.className = "ai-game-assets-designer__animation-select";
|
|
50
|
+
const animationField = labelWrap("Animation", animationSelect);
|
|
51
|
+
const promptInput = document.createElement("textarea");
|
|
52
|
+
promptInput.className = "ai-game-assets-designer__prompt";
|
|
53
|
+
promptInput.rows = 6;
|
|
54
|
+
const widthInput = numericInput();
|
|
55
|
+
const heightInput = numericInput();
|
|
56
|
+
const frameCountInput = numericInput();
|
|
57
|
+
const audioDurationInput = numericInput();
|
|
58
|
+
audioDurationInput.step = "0.1";
|
|
59
|
+
audioDurationInput.inputMode = "decimal";
|
|
60
|
+
const audioLoopInput = document.createElement("input");
|
|
61
|
+
audioLoopInput.type = "checkbox";
|
|
62
|
+
const voiceTextInput = document.createElement("textarea");
|
|
63
|
+
voiceTextInput.className = "ai-game-assets-designer__prompt";
|
|
64
|
+
voiceTextInput.rows = 3;
|
|
65
|
+
const formatSelect = document.createElement("select");
|
|
66
|
+
formatSelect.className = "ai-game-assets-designer__format-select";
|
|
67
|
+
for (const format of assetFormatOptions) {
|
|
68
|
+
const option = document.createElement("option");
|
|
69
|
+
option.value = format.value;
|
|
70
|
+
option.textContent = format.label;
|
|
71
|
+
formatSelect.append(option);
|
|
72
|
+
}
|
|
73
|
+
const audioFormatSelect = document.createElement("select");
|
|
74
|
+
audioFormatSelect.className = "ai-game-assets-designer__format-select";
|
|
75
|
+
for (const format of audioFormatOptions) {
|
|
76
|
+
const option = document.createElement("option");
|
|
77
|
+
option.value = format.value;
|
|
78
|
+
option.textContent = format.label;
|
|
79
|
+
audioFormatSelect.append(option);
|
|
80
|
+
}
|
|
81
|
+
const dimensionGrid = document.createElement("div");
|
|
82
|
+
dimensionGrid.className = "ai-game-assets-designer__dimensions";
|
|
83
|
+
dimensionGrid.append(labelWrap("Width", widthInput), labelWrap("Height", heightInput));
|
|
84
|
+
const frameCountField = labelWrap("Frames", frameCountInput);
|
|
85
|
+
const formatField = labelWrap("Format", formatSelect);
|
|
86
|
+
const audioFormatField = labelWrap("Audio format", audioFormatSelect);
|
|
87
|
+
const audioDurationField = labelWrap("Length (sec)", audioDurationInput);
|
|
88
|
+
const audioLoopField = labelWrap("Loop", audioLoopInput);
|
|
89
|
+
const voiceTextField = labelWrap("Demo sentence", voiceTextInput);
|
|
90
|
+
const currentPreview = document.createElement("div");
|
|
91
|
+
currentPreview.className = "ai-game-assets-designer__current";
|
|
92
|
+
currentPreview.setAttribute("role", "button");
|
|
93
|
+
currentPreview.tabIndex = 0;
|
|
94
|
+
const currentImage = document.createElement("img");
|
|
95
|
+
currentImage.className = "ai-game-assets-designer__current-image";
|
|
96
|
+
const currentAudio = document.createElement("div");
|
|
97
|
+
currentAudio.className = "ai-game-assets-designer__current-audio";
|
|
98
|
+
const currentAnimation = document.createElement("div");
|
|
99
|
+
currentAnimation.className = "ai-game-assets-designer__animation-stage";
|
|
100
|
+
currentAnimation.hidden = true;
|
|
101
|
+
const currentAnimationButton = document.createElement("button");
|
|
102
|
+
currentAnimationButton.type = "button";
|
|
103
|
+
currentAnimationButton.className = "ai-game-assets-designer__animate-button";
|
|
104
|
+
currentAnimationButton.textContent = "Animate";
|
|
105
|
+
currentAnimationButton.hidden = true;
|
|
106
|
+
const currentTouchUpButton = document.createElement("button");
|
|
107
|
+
currentTouchUpButton.type = "button";
|
|
108
|
+
currentTouchUpButton.className = "ai-game-assets-designer__animate-button";
|
|
109
|
+
currentTouchUpButton.textContent = "Touch up...";
|
|
110
|
+
currentTouchUpButton.hidden = true;
|
|
111
|
+
currentPreview.append(currentImage, currentAudio, currentAnimation, currentAnimationButton, currentTouchUpButton);
|
|
112
|
+
const regenerateButton = document.createElement("button");
|
|
113
|
+
regenerateButton.type = "button";
|
|
114
|
+
regenerateButton.textContent = "Regenerate";
|
|
115
|
+
const uploadButton = document.createElement("button");
|
|
116
|
+
uploadButton.type = "button";
|
|
117
|
+
uploadButton.textContent = "Upload...";
|
|
118
|
+
const promoteButton = document.createElement("button");
|
|
119
|
+
promoteButton.type = "button";
|
|
120
|
+
promoteButton.textContent = "Promote";
|
|
121
|
+
promoteButton.disabled = true;
|
|
122
|
+
const restartButton = document.createElement("button");
|
|
123
|
+
restartButton.type = "button";
|
|
124
|
+
restartButton.textContent = "Restart";
|
|
125
|
+
const actions = document.createElement("div");
|
|
126
|
+
actions.className = "ai-game-assets-designer__actions";
|
|
127
|
+
actions.append(regenerateButton, uploadButton, promoteButton, restartButton);
|
|
128
|
+
const versionLabel = document.createElement("div");
|
|
129
|
+
versionLabel.className = "ai-game-assets-designer__meta";
|
|
130
|
+
const optionsGrid = document.createElement("div");
|
|
131
|
+
optionsGrid.className = "ai-game-assets-designer__options";
|
|
132
|
+
const status = document.createElement("div");
|
|
133
|
+
status.className = "ai-game-assets-designer__status";
|
|
134
|
+
panel.append(header, labelWrap("Asset", assetBrowser), animationField, dimensionGrid, frameCountField, formatField, audioFormatField, audioDurationField, audioLoopField, voiceTextField, labelWrap("Current", currentPreview), labelWrap("Prompt", promptInput), actions, versionLabel, optionsGrid, status);
|
|
135
|
+
root.append(toggle, panel);
|
|
136
|
+
return {
|
|
137
|
+
root,
|
|
138
|
+
toggle,
|
|
139
|
+
panel,
|
|
140
|
+
styleButton,
|
|
141
|
+
assetSelect,
|
|
142
|
+
assetBrowser,
|
|
143
|
+
animationSelect,
|
|
144
|
+
animationField,
|
|
145
|
+
widthInput,
|
|
146
|
+
heightInput,
|
|
147
|
+
dimensionGrid,
|
|
148
|
+
frameCountInput,
|
|
149
|
+
frameCountField,
|
|
150
|
+
formatSelect,
|
|
151
|
+
formatField,
|
|
152
|
+
audioFormatSelect,
|
|
153
|
+
audioFormatField,
|
|
154
|
+
audioDurationInput,
|
|
155
|
+
audioDurationField,
|
|
156
|
+
audioLoopInput,
|
|
157
|
+
audioLoopField,
|
|
158
|
+
voiceTextInput,
|
|
159
|
+
voiceTextField,
|
|
160
|
+
promptInput,
|
|
161
|
+
currentImage,
|
|
162
|
+
currentAudio,
|
|
163
|
+
currentAnimation,
|
|
164
|
+
currentAnimationButton,
|
|
165
|
+
currentTouchUpButton,
|
|
166
|
+
currentPreview,
|
|
167
|
+
uploadButton,
|
|
168
|
+
regenerateButton,
|
|
169
|
+
promoteButton,
|
|
170
|
+
restartButton,
|
|
171
|
+
versionLabel,
|
|
172
|
+
options: optionsGrid,
|
|
173
|
+
status
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export function renderAssetFolderBrowser(options) {
|
|
177
|
+
const visibleAssetIds = options.assetIds ?? Object.keys(options.manifest.assets);
|
|
178
|
+
const assetPathEntries = visibleAssetIds
|
|
179
|
+
.filter((assetId) => options.manifest.assets[assetId])
|
|
180
|
+
.map((assetId) => ({
|
|
181
|
+
assetId,
|
|
182
|
+
path: folderPathForAsset(options.manifest, assetId)
|
|
183
|
+
}));
|
|
184
|
+
const selectedPath = folderPathForAsset(options.manifest, options.selectedAssetId);
|
|
185
|
+
const storedPathValue = options.container.getAttribute("data-path");
|
|
186
|
+
const storedPath = storedPathValue !== null
|
|
187
|
+
? storedPathValue.split("/").filter(Boolean)
|
|
188
|
+
: selectedPath;
|
|
189
|
+
const currentPath = pathHasAnyAsset(storedPath, assetPathEntries)
|
|
190
|
+
? storedPath
|
|
191
|
+
: selectedPath;
|
|
192
|
+
options.container.dataset.path = currentPath.join("/");
|
|
193
|
+
options.container.innerHTML = "";
|
|
194
|
+
const breadcrumbs = document.createElement("div");
|
|
195
|
+
breadcrumbs.className = "ai-game-assets-designer__asset-breadcrumbs";
|
|
196
|
+
breadcrumbs.append(assetCrumbButton("Assets", [], options));
|
|
197
|
+
currentPath.forEach((part, index) => {
|
|
198
|
+
const separator = document.createElement("span");
|
|
199
|
+
separator.textContent = "/";
|
|
200
|
+
separator.className = "ai-game-assets-designer__asset-breadcrumb-separator";
|
|
201
|
+
breadcrumbs.append(separator, assetCrumbButton(part, currentPath.slice(0, index + 1), options));
|
|
202
|
+
});
|
|
203
|
+
const list = document.createElement("div");
|
|
204
|
+
list.className = "ai-game-assets-designer__asset-list";
|
|
205
|
+
const childFolders = new Set();
|
|
206
|
+
const childAssets = [];
|
|
207
|
+
for (const entry of assetPathEntries) {
|
|
208
|
+
if (!isPathPrefix(currentPath, entry.path))
|
|
209
|
+
continue;
|
|
210
|
+
const nextPart = entry.path[currentPath.length];
|
|
211
|
+
if (nextPart) {
|
|
212
|
+
childFolders.add(nextPart);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
childAssets.push(entry.assetId);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
for (const folderName of [...childFolders].sort((a, b) => a.localeCompare(b))) {
|
|
219
|
+
const button = document.createElement("button");
|
|
220
|
+
button.type = "button";
|
|
221
|
+
button.className = "ai-game-assets-designer__asset-folder";
|
|
222
|
+
button.textContent = folderName;
|
|
223
|
+
button.addEventListener("click", () => {
|
|
224
|
+
options.container.dataset.path = [...currentPath, folderName].join("/");
|
|
225
|
+
renderAssetFolderBrowser(options);
|
|
226
|
+
});
|
|
227
|
+
list.append(button);
|
|
228
|
+
}
|
|
229
|
+
for (const assetId of childAssets.sort((a, b) => readableAssetName(a).localeCompare(readableAssetName(b)))) {
|
|
230
|
+
const button = document.createElement("button");
|
|
231
|
+
button.type = "button";
|
|
232
|
+
button.className = "ai-game-assets-designer__asset-item";
|
|
233
|
+
button.classList.toggle("is-selected", assetId === options.selectedAssetId);
|
|
234
|
+
button.textContent = readableAssetName(assetId);
|
|
235
|
+
button.addEventListener("click", () => {
|
|
236
|
+
options.container.dataset.path = folderPathForAsset(options.manifest, assetId).join("/");
|
|
237
|
+
options.onSelect(assetId);
|
|
238
|
+
});
|
|
239
|
+
list.append(button);
|
|
240
|
+
}
|
|
241
|
+
options.container.append(breadcrumbs, list);
|
|
242
|
+
}
|
|
243
|
+
export function assetCrumbButton(label, pathParts, options) {
|
|
244
|
+
const button = document.createElement("button");
|
|
245
|
+
button.type = "button";
|
|
246
|
+
button.className = "ai-game-assets-designer__asset-breadcrumb";
|
|
247
|
+
button.textContent = label;
|
|
248
|
+
button.addEventListener("click", () => {
|
|
249
|
+
options.container.dataset.path = pathParts.join("/");
|
|
250
|
+
renderAssetFolderBrowser(options);
|
|
251
|
+
});
|
|
252
|
+
return button;
|
|
253
|
+
}
|
|
254
|
+
export function folderPathForAsset(manifest, assetId) {
|
|
255
|
+
const configuredPath = manifest.assetPaths?.[assetId];
|
|
256
|
+
if (configuredPath)
|
|
257
|
+
return configuredPath;
|
|
258
|
+
const asset = manifest.assets[assetId];
|
|
259
|
+
if (!asset)
|
|
260
|
+
return [];
|
|
261
|
+
if (asset.kind === "sound")
|
|
262
|
+
return ["Sfx"];
|
|
263
|
+
if (asset.kind === "music")
|
|
264
|
+
return ["Music"];
|
|
265
|
+
if (asset.kind === "voice" || asset.kind === "voice-line")
|
|
266
|
+
return ["Voices"];
|
|
267
|
+
if (asset.id.startsWith("invader."))
|
|
268
|
+
return ["Graphics", "Invaders"];
|
|
269
|
+
if (asset.id.startsWith("ui."))
|
|
270
|
+
return ["Graphics", "UI"];
|
|
271
|
+
if (asset.id.startsWith("laser."))
|
|
272
|
+
return ["Graphics", "Lasers"];
|
|
273
|
+
if (asset.id.startsWith("background."))
|
|
274
|
+
return ["Graphics", "Background"];
|
|
275
|
+
return ["Graphics"];
|
|
276
|
+
}
|
|
277
|
+
export function pathHasAnyAsset(pathParts, entries) {
|
|
278
|
+
return entries.some((entry) => isPathPrefix(pathParts, entry.path));
|
|
279
|
+
}
|
|
280
|
+
export function isPathPrefix(prefix, fullPath) {
|
|
281
|
+
return prefix.every((part, index) => fullPath[index] === part);
|
|
282
|
+
}
|
|
283
|
+
export function renderOptions(options) {
|
|
284
|
+
options.elements.options.innerHTML = "";
|
|
285
|
+
const asset = options.manifest.assets[options.assetId];
|
|
286
|
+
const isAudio = isAudioAsset(asset);
|
|
287
|
+
options.elements.options.classList.toggle("is-audio", isAudio);
|
|
288
|
+
for (const option of options.generated) {
|
|
289
|
+
const optionAsset = assetWithGeneratedGeometry(asset, option);
|
|
290
|
+
const card = document.createElement("div");
|
|
291
|
+
card.className = "ai-game-assets-designer__option";
|
|
292
|
+
const selectButton = document.createElement("button");
|
|
293
|
+
selectButton.type = "button";
|
|
294
|
+
selectButton.className = "ai-game-assets-designer__option-select";
|
|
295
|
+
const image = document.createElement("img");
|
|
296
|
+
if (isAudio) {
|
|
297
|
+
selectButton.textContent = `Select option ${option.index + 1}`;
|
|
298
|
+
selectButton.classList.add("ai-game-assets-designer__option-select--audio");
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
image.src = option.dataUrl;
|
|
302
|
+
image.alt = `${options.assetId} option ${option.index + 1}`;
|
|
303
|
+
selectButton.append(image);
|
|
304
|
+
}
|
|
305
|
+
selectButton.addEventListener("click", () => {
|
|
306
|
+
for (const item of options.elements.options.querySelectorAll(".ai-game-assets-designer__option")) {
|
|
307
|
+
item.classList.remove("is-selected");
|
|
308
|
+
}
|
|
309
|
+
options.elements.currentPreview.classList.remove("is-selected");
|
|
310
|
+
card.classList.add("is-selected");
|
|
311
|
+
if (isAudio) {
|
|
312
|
+
options.onPreview(options.assetId, option.dataUrl, optionAsset);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
previewOption({
|
|
316
|
+
scene: options.scene,
|
|
317
|
+
manifest: options.manifest,
|
|
318
|
+
assetId: options.assetId,
|
|
319
|
+
option,
|
|
320
|
+
onPreview: options.onPreview
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
options.onSelected(option);
|
|
324
|
+
setStatus(options.elements, `Previewing option ${option.index + 1}.`, "info");
|
|
325
|
+
});
|
|
326
|
+
card.append(selectButton);
|
|
327
|
+
if (isAudio) {
|
|
328
|
+
const player = document.createElement("div");
|
|
329
|
+
player.className = "ai-game-assets-designer__option-audio";
|
|
330
|
+
renderAudioPlayer({
|
|
331
|
+
container: player,
|
|
332
|
+
src: option.dataUrl,
|
|
333
|
+
label: `${readableAssetName(options.assetId)} option ${option.index + 1}`,
|
|
334
|
+
playback: optionAsset.audioPlayback
|
|
335
|
+
});
|
|
336
|
+
card.append(player);
|
|
337
|
+
}
|
|
338
|
+
if (!isAudio && optionAsset.frameGrid) {
|
|
339
|
+
const animationStage = document.createElement("div");
|
|
340
|
+
animationStage.className = "ai-game-assets-designer__option-animation";
|
|
341
|
+
animationStage.hidden = true;
|
|
342
|
+
selectButton.append(animationStage);
|
|
343
|
+
const animateButton = document.createElement("button");
|
|
344
|
+
animateButton.type = "button";
|
|
345
|
+
animateButton.className = "ai-game-assets-designer__animate-button";
|
|
346
|
+
animateButton.textContent = "Animate";
|
|
347
|
+
let stopAnimation;
|
|
348
|
+
animateButton.addEventListener("click", () => {
|
|
349
|
+
if (stopAnimation) {
|
|
350
|
+
stopAnimation();
|
|
351
|
+
stopAnimation = undefined;
|
|
352
|
+
animationStage.hidden = true;
|
|
353
|
+
image.hidden = false;
|
|
354
|
+
animateButton.textContent = "Animate";
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
image.hidden = true;
|
|
358
|
+
animationStage.hidden = false;
|
|
359
|
+
animateButton.textContent = "Stop";
|
|
360
|
+
stopAnimation = startSpritesheetPreview({
|
|
361
|
+
element: animationStage,
|
|
362
|
+
src: option.dataUrl,
|
|
363
|
+
asset: optionAsset,
|
|
364
|
+
displaySize: resolvePreviewDisplaySize(options.designerOptions, options.assetId, optionAsset),
|
|
365
|
+
applyFrameTransforms: false
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
card.append(animateButton);
|
|
369
|
+
}
|
|
370
|
+
options.elements.options.append(card);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
export function previewOption(options) {
|
|
374
|
+
const textureKey = `ai-preview:${options.assetId}:${options.option.index}:${Date.now()}`;
|
|
375
|
+
previewImageSource({
|
|
376
|
+
scene: options.scene,
|
|
377
|
+
manifest: options.manifest,
|
|
378
|
+
assetId: options.assetId,
|
|
379
|
+
src: options.option.dataUrl,
|
|
380
|
+
textureKey,
|
|
381
|
+
assetOverride: assetWithGeneratedGeometry(options.manifest.assets[options.assetId], options.option),
|
|
382
|
+
onPreview: options.onPreview
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
export function previewCurrentAsset(options) {
|
|
386
|
+
const textureKey = `ai-current-preview:${options.assetId}:${Date.now()}`;
|
|
387
|
+
previewImageSource({
|
|
388
|
+
...options,
|
|
389
|
+
textureKey
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
export function previewImageSource(options) {
|
|
393
|
+
const image = new Image();
|
|
394
|
+
image.onload = () => {
|
|
395
|
+
if (options.scene.textures.exists(options.textureKey)) {
|
|
396
|
+
options.scene.textures.remove(options.textureKey);
|
|
397
|
+
}
|
|
398
|
+
const asset = options.assetOverride ?? options.manifest.assets[options.assetId];
|
|
399
|
+
if (asset.frameGrid && options.scene.textures.addSpriteSheet) {
|
|
400
|
+
options.scene.textures.addSpriteSheet(options.textureKey, image, {
|
|
401
|
+
frameWidth: asset.frameGrid.frameWidth,
|
|
402
|
+
frameHeight: asset.frameGrid.frameHeight,
|
|
403
|
+
margin: asset.frameGrid.margin,
|
|
404
|
+
spacing: asset.frameGrid.spacing
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
options.scene.textures.addImage(options.textureKey, image);
|
|
409
|
+
}
|
|
410
|
+
options.onPreview(options.assetId, options.textureKey, asset);
|
|
411
|
+
};
|
|
412
|
+
image.src = options.src;
|
|
413
|
+
}
|
|
414
|
+
export async function uploadedOptionFromFile(options) {
|
|
415
|
+
const dataUrl = await fileToDataUrl(options.file);
|
|
416
|
+
const mimeType = mimeTypeFromDataUrl(dataUrl);
|
|
417
|
+
const isAudio = isAudioAsset(options.asset);
|
|
418
|
+
const audioSettings = isAudio
|
|
419
|
+
? {
|
|
420
|
+
...audioGenerationOverridesFromInputs(options.elements, options.asset),
|
|
421
|
+
format: audioFormatFromMimeType(mimeType, options.file.name)
|
|
422
|
+
}
|
|
423
|
+
: undefined;
|
|
424
|
+
if (isAudio) {
|
|
425
|
+
return {
|
|
426
|
+
index: 0,
|
|
427
|
+
dataUrl,
|
|
428
|
+
mimeType,
|
|
429
|
+
prompt: options.prompt || options.asset.prompt,
|
|
430
|
+
model: "uploaded",
|
|
431
|
+
audioSettings,
|
|
432
|
+
audioPlayback: options.asset.audioPlayback,
|
|
433
|
+
voiceSettings: voiceGenerationOverridesFromInputs(options.elements, options.asset),
|
|
434
|
+
durationSeconds: await audioDurationFromDataUrl(dataUrl)
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
const imageSize = await imageSizeFromSource(dataUrl);
|
|
438
|
+
const geometry = uploadedImageGeometry(options.asset, imageSize);
|
|
439
|
+
return {
|
|
440
|
+
index: 0,
|
|
441
|
+
dataUrl,
|
|
442
|
+
mimeType,
|
|
443
|
+
prompt: options.prompt || options.asset.prompt,
|
|
444
|
+
model: "uploaded",
|
|
445
|
+
dimensions: geometry.dimensions,
|
|
446
|
+
frameGrid: geometry.frameGrid,
|
|
447
|
+
animations: options.asset.animations,
|
|
448
|
+
settings: {
|
|
449
|
+
...options.asset.settings,
|
|
450
|
+
format: normalizeAssetFormatFromMimeType(mimeType, options.file.name)
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
export function uploadedImageGeometry(asset, imageSize) {
|
|
455
|
+
if (!asset.frameGrid) {
|
|
456
|
+
return {
|
|
457
|
+
dimensions: imageSize
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
const frameGrid = {
|
|
461
|
+
...asset.frameGrid
|
|
462
|
+
};
|
|
463
|
+
return {
|
|
464
|
+
dimensions: {
|
|
465
|
+
width: frameGrid.frameWidth * frameGrid.columns,
|
|
466
|
+
height: frameGrid.frameHeight * frameGrid.rows
|
|
467
|
+
},
|
|
468
|
+
frameGrid
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
export function pickUploadFile(accept) {
|
|
472
|
+
return new Promise((resolve) => {
|
|
473
|
+
const input = document.createElement("input");
|
|
474
|
+
input.type = "file";
|
|
475
|
+
input.accept = accept;
|
|
476
|
+
input.addEventListener("change", () => {
|
|
477
|
+
resolve(input.files?.[0]);
|
|
478
|
+
}, { once: true });
|
|
479
|
+
input.click();
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
export function imageSizeFromSource(src) {
|
|
483
|
+
return new Promise((resolve, reject) => {
|
|
484
|
+
const image = new Image();
|
|
485
|
+
image.onload = () => resolve({
|
|
486
|
+
width: Math.max(1, image.naturalWidth),
|
|
487
|
+
height: Math.max(1, image.naturalHeight)
|
|
488
|
+
});
|
|
489
|
+
image.onerror = () => reject(new Error("Could not load uploaded image."));
|
|
490
|
+
image.src = src;
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
export async function replaceSpriteSheetFrames(options) {
|
|
494
|
+
const [sheetImage, frameImage] = await Promise.all([
|
|
495
|
+
loadImageElement(options.src),
|
|
496
|
+
loadImageElement(options.uploadSrc)
|
|
497
|
+
]);
|
|
498
|
+
const frameWidth = options.frameGrid.frameWidth;
|
|
499
|
+
const frameHeight = options.frameGrid.frameHeight;
|
|
500
|
+
const margin = options.frameGrid.margin ?? 0;
|
|
501
|
+
const spacing = options.frameGrid.spacing ?? 0;
|
|
502
|
+
const canvas = document.createElement("canvas");
|
|
503
|
+
canvas.width = margin * 2 +
|
|
504
|
+
(options.frameGrid.columns * frameWidth) +
|
|
505
|
+
(Math.max(0, options.frameGrid.columns - 1) * spacing);
|
|
506
|
+
canvas.height = margin * 2 +
|
|
507
|
+
(options.frameGrid.rows * frameHeight) +
|
|
508
|
+
(Math.max(0, options.frameGrid.rows - 1) * spacing);
|
|
509
|
+
const context = canvas.getContext("2d");
|
|
510
|
+
if (!context) {
|
|
511
|
+
throw new Error("Could not create canvas for frame upload.");
|
|
512
|
+
}
|
|
513
|
+
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
514
|
+
context.drawImage(sheetImage, 0, 0, canvas.width, canvas.height);
|
|
515
|
+
for (const frame of options.frames) {
|
|
516
|
+
const column = frame % options.frameGrid.columns;
|
|
517
|
+
const row = Math.floor(frame / options.frameGrid.columns);
|
|
518
|
+
const x = margin + (column * (frameWidth + spacing));
|
|
519
|
+
const y = margin + (row * (frameHeight + spacing));
|
|
520
|
+
context.clearRect(x, y, frameWidth, frameHeight);
|
|
521
|
+
context.drawImage(frameImage, x, y, frameWidth, frameHeight);
|
|
522
|
+
}
|
|
523
|
+
return canvas.toDataURL("image/png");
|
|
524
|
+
}
|
|
525
|
+
export async function spriteSheetFrameToDataUrl(options) {
|
|
526
|
+
const sheetImage = await loadImageElement(options.src);
|
|
527
|
+
const frameWidth = options.frameGrid.frameWidth;
|
|
528
|
+
const frameHeight = options.frameGrid.frameHeight;
|
|
529
|
+
const margin = options.frameGrid.margin ?? 0;
|
|
530
|
+
const spacing = options.frameGrid.spacing ?? 0;
|
|
531
|
+
const column = options.frame % options.frameGrid.columns;
|
|
532
|
+
const row = Math.floor(options.frame / options.frameGrid.columns);
|
|
533
|
+
const sourceX = margin + (column * (frameWidth + spacing));
|
|
534
|
+
const sourceY = margin + (row * (frameHeight + spacing));
|
|
535
|
+
const canvas = document.createElement("canvas");
|
|
536
|
+
canvas.width = frameWidth;
|
|
537
|
+
canvas.height = frameHeight;
|
|
538
|
+
const context = canvas.getContext("2d");
|
|
539
|
+
if (!context) {
|
|
540
|
+
throw new Error("Could not create canvas for frame touch-up.");
|
|
541
|
+
}
|
|
542
|
+
context.clearRect(0, 0, frameWidth, frameHeight);
|
|
543
|
+
context.drawImage(sheetImage, sourceX, sourceY, frameWidth, frameHeight, 0, 0, frameWidth, frameHeight);
|
|
544
|
+
return canvas.toDataURL("image/png");
|
|
545
|
+
}
|
|
546
|
+
export function isSvgSource(src) {
|
|
547
|
+
return src.startsWith("data:image/svg+xml") || /\.svg(?:$|\?)/i.test(src);
|
|
548
|
+
}
|
|
549
|
+
export async function openFrameTouchUpEditor(options) {
|
|
550
|
+
const sourceImage = await loadImageElement(options.frameSrc);
|
|
551
|
+
const dialog = document.createElement("div");
|
|
552
|
+
dialog.className = "ai-game-assets-designer__touchup";
|
|
553
|
+
dialog.setAttribute("role", "dialog");
|
|
554
|
+
dialog.setAttribute("aria-modal", "true");
|
|
555
|
+
dialog.setAttribute("aria-label", `Touch up ${options.title}`);
|
|
556
|
+
const header = document.createElement("div");
|
|
557
|
+
header.className = "ai-game-assets-designer__touchup-header";
|
|
558
|
+
const title = document.createElement("div");
|
|
559
|
+
title.className = "ai-game-assets-designer__touchup-title";
|
|
560
|
+
title.textContent = options.title;
|
|
561
|
+
const dirtyLabel = document.createElement("span");
|
|
562
|
+
dirtyLabel.className = "ai-game-assets-designer__touchup-dirty";
|
|
563
|
+
dirtyLabel.hidden = true;
|
|
564
|
+
dirtyLabel.textContent = "Unsaved changes";
|
|
565
|
+
const closeButton = document.createElement("button");
|
|
566
|
+
closeButton.type = "button";
|
|
567
|
+
closeButton.className = "ai-game-assets-designer__touchup-close";
|
|
568
|
+
closeButton.setAttribute("aria-label", "Close touch-up editor");
|
|
569
|
+
closeButton.textContent = "X";
|
|
570
|
+
header.append(title, dirtyLabel, closeButton);
|
|
571
|
+
const toolbar = document.createElement("div");
|
|
572
|
+
toolbar.className = "ai-game-assets-designer__touchup-toolbar";
|
|
573
|
+
const zoomOutButton = touchUpButton("Zoom -");
|
|
574
|
+
const zoomInButton = touchUpButton("Zoom +");
|
|
575
|
+
const brushButton = touchUpButton("Brush");
|
|
576
|
+
const brushMenuButton = touchUpButton("");
|
|
577
|
+
brushMenuButton.className = "ai-game-assets-designer__touchup-split-arrow";
|
|
578
|
+
brushMenuButton.setAttribute("aria-label", "Brush settings");
|
|
579
|
+
const brushSplit = document.createElement("div");
|
|
580
|
+
brushSplit.className = "ai-game-assets-designer__touchup-split";
|
|
581
|
+
brushSplit.append(brushButton, brushMenuButton);
|
|
582
|
+
const brushMenu = document.createElement("div");
|
|
583
|
+
brushMenu.className = "ai-game-assets-designer__touchup-brush-menu";
|
|
584
|
+
brushMenu.hidden = true;
|
|
585
|
+
const brushSizeInput = document.createElement("input");
|
|
586
|
+
brushSizeInput.type = "range";
|
|
587
|
+
brushSizeInput.min = "1";
|
|
588
|
+
brushSizeInput.max = "32";
|
|
589
|
+
brushSizeInput.step = "1";
|
|
590
|
+
brushSizeInput.value = "4";
|
|
591
|
+
const brushSizeValue = document.createElement("span");
|
|
592
|
+
brushSizeValue.textContent = brushSizeInput.value;
|
|
593
|
+
const brushSizeField = document.createElement("label");
|
|
594
|
+
brushSizeField.className = "ai-game-assets-designer__touchup-brush-size";
|
|
595
|
+
brushSizeField.append("Size", brushSizeInput, brushSizeValue);
|
|
596
|
+
const antiAliasInput = document.createElement("input");
|
|
597
|
+
antiAliasInput.type = "checkbox";
|
|
598
|
+
antiAliasInput.checked = true;
|
|
599
|
+
const antiAliasField = document.createElement("label");
|
|
600
|
+
antiAliasField.className = "ai-game-assets-designer__inline-checkbox";
|
|
601
|
+
antiAliasField.append(antiAliasInput, "Anti-aliased round tip");
|
|
602
|
+
brushMenu.append(brushSizeField, antiAliasField);
|
|
603
|
+
brushSplit.append(brushMenu);
|
|
604
|
+
const eraserButton = touchUpButton("Eraser");
|
|
605
|
+
const pickerButton = touchUpButton("Picker");
|
|
606
|
+
const selectButton = touchUpButton("Select");
|
|
607
|
+
const fillButton = touchUpButton("Fill");
|
|
608
|
+
const undoButton = touchUpButton("Undo");
|
|
609
|
+
const redoButton = touchUpButton("Redo");
|
|
610
|
+
const saveButton = touchUpButton("Save");
|
|
611
|
+
saveButton.classList.add("is-primary");
|
|
612
|
+
const colorInput = document.createElement("input");
|
|
613
|
+
colorInput.type = "color";
|
|
614
|
+
colorInput.value = "#f8fafc";
|
|
615
|
+
colorInput.setAttribute("aria-label", "Brush color");
|
|
616
|
+
const alphaInput = document.createElement("input");
|
|
617
|
+
alphaInput.type = "number";
|
|
618
|
+
alphaInput.min = "0";
|
|
619
|
+
alphaInput.max = "255";
|
|
620
|
+
alphaInput.step = "1";
|
|
621
|
+
alphaInput.value = "255";
|
|
622
|
+
alphaInput.setAttribute("aria-label", "Alpha");
|
|
623
|
+
const rgbaLabel = document.createElement("label");
|
|
624
|
+
rgbaLabel.className = "ai-game-assets-designer__touchup-color";
|
|
625
|
+
const alphaLabel = document.createElement("span");
|
|
626
|
+
alphaLabel.textContent = "A";
|
|
627
|
+
rgbaLabel.append(colorInput, alphaLabel, alphaInput);
|
|
628
|
+
toolbar.append(zoomOutButton, zoomInButton, rgbaLabel, pickerButton, brushSplit, eraserButton, selectButton, fillButton, undoButton, redoButton, saveButton);
|
|
629
|
+
const workspace = document.createElement("div");
|
|
630
|
+
workspace.className = "ai-game-assets-designer__touchup-workspace";
|
|
631
|
+
const canvasWrap = document.createElement("div");
|
|
632
|
+
canvasWrap.className = "ai-game-assets-designer__touchup-canvas-wrap";
|
|
633
|
+
const canvas = document.createElement("canvas");
|
|
634
|
+
canvas.className = "ai-game-assets-designer__touchup-canvas";
|
|
635
|
+
canvas.width = Math.max(1, sourceImage.naturalWidth);
|
|
636
|
+
canvas.height = Math.max(1, sourceImage.naturalHeight);
|
|
637
|
+
const selectionBox = document.createElement("div");
|
|
638
|
+
selectionBox.className = "ai-game-assets-designer__touchup-selection";
|
|
639
|
+
selectionBox.hidden = true;
|
|
640
|
+
canvasWrap.append(canvas, selectionBox);
|
|
641
|
+
const side = document.createElement("div");
|
|
642
|
+
side.className = "ai-game-assets-designer__touchup-side";
|
|
643
|
+
const stillPanel = document.createElement("div");
|
|
644
|
+
stillPanel.className = "ai-game-assets-designer__touchup-panel";
|
|
645
|
+
const stillTitle = document.createElement("div");
|
|
646
|
+
stillTitle.textContent = "Frame";
|
|
647
|
+
const stillPreview = document.createElement("canvas");
|
|
648
|
+
stillPreview.width = canvas.width;
|
|
649
|
+
stillPreview.height = canvas.height;
|
|
650
|
+
stillPanel.append(stillTitle, stillPreview);
|
|
651
|
+
const animationPanel = document.createElement("div");
|
|
652
|
+
animationPanel.className = "ai-game-assets-designer__touchup-panel";
|
|
653
|
+
const animationTitle = document.createElement("div");
|
|
654
|
+
animationTitle.textContent = "Animation";
|
|
655
|
+
const animationPreview = document.createElement("canvas");
|
|
656
|
+
animationPreview.width = Math.max(1, Math.round(options.displaySize.width));
|
|
657
|
+
animationPreview.height = Math.max(1, Math.round(options.displaySize.height));
|
|
658
|
+
animationPanel.append(animationTitle, animationPreview);
|
|
659
|
+
side.append(stillPanel, animationPanel);
|
|
660
|
+
workspace.append(canvasWrap, side);
|
|
661
|
+
dialog.append(header, toolbar, workspace);
|
|
662
|
+
options.root.append(dialog);
|
|
663
|
+
const context = canvas.getContext("2d", { willReadFrequently: true });
|
|
664
|
+
const stillContext = stillPreview.getContext("2d");
|
|
665
|
+
const animationContext = animationPreview.getContext("2d");
|
|
666
|
+
if (!context || !stillContext || !animationContext) {
|
|
667
|
+
dialog.remove();
|
|
668
|
+
throw new Error("Could not create touch-up canvas.");
|
|
669
|
+
}
|
|
670
|
+
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
671
|
+
context.drawImage(sourceImage, 0, 0, canvas.width, canvas.height);
|
|
672
|
+
let zoom = Math.max(1, Math.floor(Math.min(8, 480 / Math.max(canvas.width, canvas.height))));
|
|
673
|
+
let tool = "brush";
|
|
674
|
+
let dirty = false;
|
|
675
|
+
let drawing = false;
|
|
676
|
+
let selectionStart;
|
|
677
|
+
let selection;
|
|
678
|
+
let animationTimeout;
|
|
679
|
+
let animationCursor = 0;
|
|
680
|
+
let disposed = false;
|
|
681
|
+
const activeTouchPointers = new Map();
|
|
682
|
+
let pinchStartDistance;
|
|
683
|
+
let pinchStartZoom = zoom;
|
|
684
|
+
const undoStack = [];
|
|
685
|
+
const redoStack = [];
|
|
686
|
+
const frameGrid = options.asset.frameGrid;
|
|
687
|
+
const animation = options.asset.animations?.[0];
|
|
688
|
+
const frames = animation?.frames ?? [options.frame ?? 0];
|
|
689
|
+
const frameRate = animation?.frameRate ?? 8;
|
|
690
|
+
const frameTimings = animation?.frameTimings ?? [];
|
|
691
|
+
const sheetImage = options.spriteSheetSrc
|
|
692
|
+
? await loadImageElement(options.spriteSheetSrc)
|
|
693
|
+
: undefined;
|
|
694
|
+
const selectedColor = () => {
|
|
695
|
+
const hex = colorInput.value.replace("#", "");
|
|
696
|
+
const red = Number.parseInt(hex.slice(0, 2), 16);
|
|
697
|
+
const green = Number.parseInt(hex.slice(2, 4), 16);
|
|
698
|
+
const blue = Number.parseInt(hex.slice(4, 6), 16);
|
|
699
|
+
const alpha = clamp(Math.round(Number(alphaInput.value)), 0, 255);
|
|
700
|
+
return [red, green, blue, alpha];
|
|
701
|
+
};
|
|
702
|
+
const brushSize = () => positiveIntegerInput(brushSizeInput, 1);
|
|
703
|
+
const setDirty = (value) => {
|
|
704
|
+
dirty = value;
|
|
705
|
+
dirtyLabel.hidden = !dirty;
|
|
706
|
+
};
|
|
707
|
+
const updateZoom = () => {
|
|
708
|
+
canvas.style.width = `${canvas.width * zoom}px`;
|
|
709
|
+
canvas.style.height = `${canvas.height * zoom}px`;
|
|
710
|
+
syncSelectionBox();
|
|
711
|
+
};
|
|
712
|
+
const updateToolButtons = () => {
|
|
713
|
+
for (const button of [brushButton, eraserButton, pickerButton, selectButton, fillButton]) {
|
|
714
|
+
button.classList.remove("is-active");
|
|
715
|
+
}
|
|
716
|
+
const activeButton = {
|
|
717
|
+
brush: brushButton,
|
|
718
|
+
eraser: eraserButton,
|
|
719
|
+
picker: pickerButton,
|
|
720
|
+
select: selectButton,
|
|
721
|
+
fill: fillButton
|
|
722
|
+
}[tool];
|
|
723
|
+
activeButton.classList.add("is-active");
|
|
724
|
+
};
|
|
725
|
+
const updateUndoRedo = () => {
|
|
726
|
+
undoButton.disabled = undoStack.length === 0;
|
|
727
|
+
redoButton.disabled = redoStack.length === 0;
|
|
728
|
+
};
|
|
729
|
+
const snapshot = () => context.getImageData(0, 0, canvas.width, canvas.height);
|
|
730
|
+
const pushUndo = () => {
|
|
731
|
+
undoStack.push(snapshot());
|
|
732
|
+
if (undoStack.length > 60)
|
|
733
|
+
undoStack.shift();
|
|
734
|
+
redoStack.length = 0;
|
|
735
|
+
updateUndoRedo();
|
|
736
|
+
};
|
|
737
|
+
const restore = (imageData) => {
|
|
738
|
+
context.putImageData(imageData, 0, 0);
|
|
739
|
+
selection = undefined;
|
|
740
|
+
redrawEditor();
|
|
741
|
+
setDirty(true);
|
|
742
|
+
};
|
|
743
|
+
const syncSelectionBox = () => {
|
|
744
|
+
if (!selection) {
|
|
745
|
+
selectionBox.hidden = true;
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
selectionBox.hidden = false;
|
|
749
|
+
selectionBox.style.left = `${selection.x * zoom}px`;
|
|
750
|
+
selectionBox.style.top = `${selection.y * zoom}px`;
|
|
751
|
+
selectionBox.style.width = `${selection.width * zoom}px`;
|
|
752
|
+
selectionBox.style.height = `${selection.height * zoom}px`;
|
|
753
|
+
};
|
|
754
|
+
const refreshPreviews = () => {
|
|
755
|
+
stillContext.clearRect(0, 0, stillPreview.width, stillPreview.height);
|
|
756
|
+
stillContext.imageSmoothingEnabled = false;
|
|
757
|
+
stillContext.drawImage(canvas, 0, 0, stillPreview.width, stillPreview.height);
|
|
758
|
+
};
|
|
759
|
+
const redrawEditor = () => {
|
|
760
|
+
refreshPreviews();
|
|
761
|
+
syncSelectionBox();
|
|
762
|
+
};
|
|
763
|
+
const canvasPoint = (event) => {
|
|
764
|
+
const bounds = canvas.getBoundingClientRect();
|
|
765
|
+
return {
|
|
766
|
+
x: clamp(Math.floor(((event.clientX - bounds.left) / bounds.width) * canvas.width), 0, canvas.width - 1),
|
|
767
|
+
y: clamp(Math.floor(((event.clientY - bounds.top) / bounds.height) * canvas.height), 0, canvas.height - 1)
|
|
768
|
+
};
|
|
769
|
+
};
|
|
770
|
+
const setZoom = (value, anchor) => {
|
|
771
|
+
const previousZoom = zoom;
|
|
772
|
+
let anchorCanvasX;
|
|
773
|
+
let anchorCanvasY;
|
|
774
|
+
let anchorOffsetX;
|
|
775
|
+
let anchorOffsetY;
|
|
776
|
+
if (anchor) {
|
|
777
|
+
const canvasBounds = canvas.getBoundingClientRect();
|
|
778
|
+
const wrapBounds = canvasWrap.getBoundingClientRect();
|
|
779
|
+
anchorCanvasX = ((anchor.clientX - canvasBounds.left) / canvasBounds.width) * canvas.width;
|
|
780
|
+
anchorCanvasY = ((anchor.clientY - canvasBounds.top) / canvasBounds.height) * canvas.height;
|
|
781
|
+
anchorOffsetX = anchor.clientX - wrapBounds.left;
|
|
782
|
+
anchorOffsetY = anchor.clientY - wrapBounds.top;
|
|
783
|
+
}
|
|
784
|
+
zoom = clamp(value, 1, 32);
|
|
785
|
+
if (zoom === previousZoom)
|
|
786
|
+
return;
|
|
787
|
+
updateZoom();
|
|
788
|
+
if (anchorCanvasX !== undefined &&
|
|
789
|
+
anchorCanvasY !== undefined &&
|
|
790
|
+
anchorOffsetX !== undefined &&
|
|
791
|
+
anchorOffsetY !== undefined) {
|
|
792
|
+
canvasWrap.scrollLeft = (anchorCanvasX * zoom) + canvas.offsetLeft - anchorOffsetX;
|
|
793
|
+
canvasWrap.scrollTop = (anchorCanvasY * zoom) + canvas.offsetTop - anchorOffsetY;
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
const touchPointerDistance = () => {
|
|
797
|
+
const pointers = [...activeTouchPointers.values()];
|
|
798
|
+
const first = pointers[0];
|
|
799
|
+
const second = pointers[1];
|
|
800
|
+
if (!first || !second)
|
|
801
|
+
return undefined;
|
|
802
|
+
return Math.hypot(second.x - first.x, second.y - first.y);
|
|
803
|
+
};
|
|
804
|
+
const touchPointerCenter = () => {
|
|
805
|
+
const pointers = [...activeTouchPointers.values()];
|
|
806
|
+
const first = pointers[0];
|
|
807
|
+
const second = pointers[1];
|
|
808
|
+
if (!first || !second)
|
|
809
|
+
return undefined;
|
|
810
|
+
return {
|
|
811
|
+
clientX: (first.x + second.x) / 2,
|
|
812
|
+
clientY: (first.y + second.y) / 2
|
|
813
|
+
};
|
|
814
|
+
};
|
|
815
|
+
const paintAt = (point) => {
|
|
816
|
+
const size = brushSize();
|
|
817
|
+
const halfSize = Math.floor(size / 2);
|
|
818
|
+
const left = clamp(point.x - halfSize, 0, canvas.width - 1);
|
|
819
|
+
const top = clamp(point.y - halfSize, 0, canvas.height - 1);
|
|
820
|
+
const right = clamp(left + size - 1, 0, canvas.width - 1);
|
|
821
|
+
const bottom = clamp(top + size - 1, 0, canvas.height - 1);
|
|
822
|
+
const imageData = context.getImageData(left, top, right - left + 1, bottom - top + 1);
|
|
823
|
+
const data = imageData.data;
|
|
824
|
+
const color = selectedColor();
|
|
825
|
+
const antiAliased = antiAliasInput.checked;
|
|
826
|
+
const radius = size / 2;
|
|
827
|
+
const centerX = point.x + 0.5;
|
|
828
|
+
const centerY = point.y + 0.5;
|
|
829
|
+
for (let y = 0; y < imageData.height; y += 1) {
|
|
830
|
+
for (let x = 0; x < imageData.width; x += 1) {
|
|
831
|
+
const canvasX = left + x;
|
|
832
|
+
const canvasY = top + y;
|
|
833
|
+
let coverage = 1;
|
|
834
|
+
if (antiAliased) {
|
|
835
|
+
const deltaX = (canvasX + 0.5) - centerX;
|
|
836
|
+
const deltaY = (canvasY + 0.5) - centerY;
|
|
837
|
+
const distance = Math.hypot(deltaX, deltaY);
|
|
838
|
+
coverage = clamp(radius + 0.5 - distance, 0, 1);
|
|
839
|
+
if (coverage <= 0)
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
const index = ((y * imageData.width) + x) * 4;
|
|
843
|
+
if (tool === "eraser") {
|
|
844
|
+
if (antiAliased && coverage < 1) {
|
|
845
|
+
data[index + 3] = Math.round(data[index + 3] * (1 - coverage));
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
data[index] = 0;
|
|
849
|
+
data[index + 1] = 0;
|
|
850
|
+
data[index + 2] = 0;
|
|
851
|
+
data[index + 3] = 0;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
else {
|
|
855
|
+
if (antiAliased && coverage < 1) {
|
|
856
|
+
const sourceAlpha = (color[3] / 255) * coverage;
|
|
857
|
+
const targetAlpha = data[index + 3] / 255;
|
|
858
|
+
const outAlpha = sourceAlpha + (targetAlpha * (1 - sourceAlpha));
|
|
859
|
+
if (outAlpha <= 0) {
|
|
860
|
+
data[index] = 0;
|
|
861
|
+
data[index + 1] = 0;
|
|
862
|
+
data[index + 2] = 0;
|
|
863
|
+
data[index + 3] = 0;
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
data[index] = Math.round(((color[0] * sourceAlpha) + (data[index] * targetAlpha * (1 - sourceAlpha))) / outAlpha);
|
|
867
|
+
data[index + 1] = Math.round(((color[1] * sourceAlpha) + (data[index + 1] * targetAlpha * (1 - sourceAlpha))) / outAlpha);
|
|
868
|
+
data[index + 2] = Math.round(((color[2] * sourceAlpha) + (data[index + 2] * targetAlpha * (1 - sourceAlpha))) / outAlpha);
|
|
869
|
+
data[index + 3] = Math.round(outAlpha * 255);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
873
|
+
data[index] = color[0];
|
|
874
|
+
data[index + 1] = color[1];
|
|
875
|
+
data[index + 2] = color[2];
|
|
876
|
+
data[index + 3] = color[3];
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
context.putImageData(imageData, left, top);
|
|
882
|
+
};
|
|
883
|
+
const pickColorAt = (point) => {
|
|
884
|
+
const data = context.getImageData(point.x, point.y, 1, 1).data;
|
|
885
|
+
colorInput.value = `#${hexByte(data[0])}${hexByte(data[1])}${hexByte(data[2])}`;
|
|
886
|
+
alphaInput.value = String(data[3]);
|
|
887
|
+
};
|
|
888
|
+
const fillAt = (point) => {
|
|
889
|
+
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
|
890
|
+
const data = imageData.data;
|
|
891
|
+
const replacement = selectedColor();
|
|
892
|
+
const startIndex = ((point.y * canvas.width) + point.x) * 4;
|
|
893
|
+
const target = [
|
|
894
|
+
data[startIndex],
|
|
895
|
+
data[startIndex + 1],
|
|
896
|
+
data[startIndex + 2],
|
|
897
|
+
data[startIndex + 3]
|
|
898
|
+
];
|
|
899
|
+
if (target.every((value, index) => value === replacement[index]))
|
|
900
|
+
return;
|
|
901
|
+
const stack = [point];
|
|
902
|
+
const visited = new Uint8Array(canvas.width * canvas.height);
|
|
903
|
+
while (stack.length > 0) {
|
|
904
|
+
const current = stack.pop();
|
|
905
|
+
if (!current)
|
|
906
|
+
continue;
|
|
907
|
+
if (current.x < 0 || current.y < 0 || current.x >= canvas.width || current.y >= canvas.height)
|
|
908
|
+
continue;
|
|
909
|
+
const pixelIndex = (current.y * canvas.width) + current.x;
|
|
910
|
+
if (visited[pixelIndex])
|
|
911
|
+
continue;
|
|
912
|
+
visited[pixelIndex] = 1;
|
|
913
|
+
const dataIndex = pixelIndex * 4;
|
|
914
|
+
if (data[dataIndex] !== target[0] ||
|
|
915
|
+
data[dataIndex + 1] !== target[1] ||
|
|
916
|
+
data[dataIndex + 2] !== target[2] ||
|
|
917
|
+
data[dataIndex + 3] !== target[3]) {
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
data[dataIndex] = replacement[0];
|
|
921
|
+
data[dataIndex + 1] = replacement[1];
|
|
922
|
+
data[dataIndex + 2] = replacement[2];
|
|
923
|
+
data[dataIndex + 3] = replacement[3];
|
|
924
|
+
stack.push({ x: current.x + 1, y: current.y }, { x: current.x - 1, y: current.y }, { x: current.x, y: current.y + 1 }, { x: current.x, y: current.y - 1 });
|
|
925
|
+
}
|
|
926
|
+
context.putImageData(imageData, 0, 0);
|
|
927
|
+
};
|
|
928
|
+
const moveSelection = (deltaX, deltaY) => {
|
|
929
|
+
if (!selection)
|
|
930
|
+
return;
|
|
931
|
+
pushUndo();
|
|
932
|
+
const nextX = clamp(selection.x + deltaX, 0, canvas.width - selection.width);
|
|
933
|
+
const nextY = clamp(selection.y + deltaY, 0, canvas.height - selection.height);
|
|
934
|
+
const imageData = context.getImageData(selection.x, selection.y, selection.width, selection.height);
|
|
935
|
+
context.clearRect(selection.x, selection.y, selection.width, selection.height);
|
|
936
|
+
context.putImageData(imageData, nextX, nextY);
|
|
937
|
+
selection = { ...selection, x: nextX, y: nextY };
|
|
938
|
+
setDirty(true);
|
|
939
|
+
redrawEditor();
|
|
940
|
+
};
|
|
941
|
+
const deleteSelection = () => {
|
|
942
|
+
if (!selection)
|
|
943
|
+
return;
|
|
944
|
+
pushUndo();
|
|
945
|
+
context.clearRect(selection.x, selection.y, selection.width, selection.height);
|
|
946
|
+
selection = undefined;
|
|
947
|
+
setDirty(true);
|
|
948
|
+
redrawEditor();
|
|
949
|
+
};
|
|
950
|
+
const drawAnimationFrame = () => {
|
|
951
|
+
if (disposed)
|
|
952
|
+
return;
|
|
953
|
+
if (!frameGrid || !animation || !sheetImage) {
|
|
954
|
+
animationContext.clearRect(0, 0, animationPreview.width, animationPreview.height);
|
|
955
|
+
animationContext.imageSmoothingEnabled = false;
|
|
956
|
+
animationContext.drawImage(canvas, 0, 0, animationPreview.width, animationPreview.height);
|
|
957
|
+
animationTimeout = window.setTimeout(drawAnimationFrame, 250);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const frameSlot = animationCursor % frames.length;
|
|
961
|
+
const frame = frames[frameSlot] ?? 0;
|
|
962
|
+
const timing = frameTimings[frameSlot];
|
|
963
|
+
const column = frame % frameGrid.columns;
|
|
964
|
+
const row = Math.floor(frame / frameGrid.columns);
|
|
965
|
+
const sourceX = (frameGrid.margin ?? 0) + (column * (frameGrid.frameWidth + (frameGrid.spacing ?? 0)));
|
|
966
|
+
const sourceY = (frameGrid.margin ?? 0) + (row * (frameGrid.frameHeight + (frameGrid.spacing ?? 0)));
|
|
967
|
+
animationContext.clearRect(0, 0, animationPreview.width, animationPreview.height);
|
|
968
|
+
animationContext.imageSmoothingEnabled = false;
|
|
969
|
+
animationContext.save();
|
|
970
|
+
animationContext.translate(animationPreview.width / 2, animationPreview.height / 2);
|
|
971
|
+
animationContext.translate(timing?.offsetX ?? 0, timing?.offsetY ?? 0);
|
|
972
|
+
animationContext.scale(timing?.scaleX ?? 1, timing?.scaleY ?? 1);
|
|
973
|
+
animationContext.rotate(((timing?.rotation ?? 0) * Math.PI) / 180);
|
|
974
|
+
if (frameSlot === options.frameSlot) {
|
|
975
|
+
animationContext.drawImage(canvas, -options.displaySize.width / 2, -options.displaySize.height / 2, options.displaySize.width, options.displaySize.height);
|
|
976
|
+
}
|
|
977
|
+
else {
|
|
978
|
+
animationContext.drawImage(sheetImage, sourceX, sourceY, frameGrid.frameWidth, frameGrid.frameHeight, -options.displaySize.width / 2, -options.displaySize.height / 2, options.displaySize.width, options.displaySize.height);
|
|
979
|
+
}
|
|
980
|
+
animationContext.restore();
|
|
981
|
+
animationCursor += 1;
|
|
982
|
+
animationTimeout = window.setTimeout(drawAnimationFrame, timing?.delayMs ?? 1000 / frameRate);
|
|
983
|
+
};
|
|
984
|
+
const closeBrushMenuOnOutsidePointer = (event) => {
|
|
985
|
+
const target = event.target;
|
|
986
|
+
if (brushMenu.hidden || !(target instanceof Node) || brushSplit.contains(target))
|
|
987
|
+
return;
|
|
988
|
+
brushMenu.hidden = true;
|
|
989
|
+
};
|
|
990
|
+
const close = () => {
|
|
991
|
+
disposed = true;
|
|
992
|
+
if (animationTimeout !== undefined)
|
|
993
|
+
window.clearTimeout(animationTimeout);
|
|
994
|
+
window.removeEventListener("keydown", keyHandler);
|
|
995
|
+
document.removeEventListener("pointerdown", closeBrushMenuOnOutsidePointer);
|
|
996
|
+
dialog.remove();
|
|
997
|
+
};
|
|
998
|
+
const requestClose = () => {
|
|
999
|
+
if (dirty && !window.confirm("You have unsaved changes. Close without saving?"))
|
|
1000
|
+
return;
|
|
1001
|
+
close();
|
|
1002
|
+
};
|
|
1003
|
+
const keyHandler = (event) => {
|
|
1004
|
+
if (!dialog.isConnected)
|
|
1005
|
+
return;
|
|
1006
|
+
if (event.key === "Escape") {
|
|
1007
|
+
requestClose();
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
if (event.key === "Backspace" || event.key === "Delete") {
|
|
1011
|
+
event.preventDefault();
|
|
1012
|
+
deleteSelection();
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (event.key === "ArrowLeft") {
|
|
1016
|
+
event.preventDefault();
|
|
1017
|
+
moveSelection(-1, 0);
|
|
1018
|
+
}
|
|
1019
|
+
else if (event.key === "ArrowRight") {
|
|
1020
|
+
event.preventDefault();
|
|
1021
|
+
moveSelection(1, 0);
|
|
1022
|
+
}
|
|
1023
|
+
else if (event.key === "ArrowUp") {
|
|
1024
|
+
event.preventDefault();
|
|
1025
|
+
moveSelection(0, -1);
|
|
1026
|
+
}
|
|
1027
|
+
else if (event.key === "ArrowDown") {
|
|
1028
|
+
event.preventDefault();
|
|
1029
|
+
moveSelection(0, 1);
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
canvas.addEventListener("pointerdown", (event) => {
|
|
1033
|
+
if (event.pointerType === "touch") {
|
|
1034
|
+
activeTouchPointers.set(event.pointerId, {
|
|
1035
|
+
x: event.clientX,
|
|
1036
|
+
y: event.clientY
|
|
1037
|
+
});
|
|
1038
|
+
if (activeTouchPointers.size >= 2) {
|
|
1039
|
+
drawing = false;
|
|
1040
|
+
selectionStart = undefined;
|
|
1041
|
+
pinchStartDistance = touchPointerDistance();
|
|
1042
|
+
pinchStartZoom = zoom;
|
|
1043
|
+
canvas.setPointerCapture(event.pointerId);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
const point = canvasPoint(event);
|
|
1048
|
+
if (tool === "picker") {
|
|
1049
|
+
pickColorAt(point);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
pushUndo();
|
|
1053
|
+
drawing = true;
|
|
1054
|
+
canvas.setPointerCapture(event.pointerId);
|
|
1055
|
+
if (tool === "select") {
|
|
1056
|
+
selectionStart = point;
|
|
1057
|
+
selection = { x: point.x, y: point.y, width: 1, height: 1 };
|
|
1058
|
+
redrawEditor();
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
if (tool === "fill") {
|
|
1062
|
+
fillAt(point);
|
|
1063
|
+
drawing = false;
|
|
1064
|
+
setDirty(true);
|
|
1065
|
+
redrawEditor();
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
paintAt(point);
|
|
1069
|
+
setDirty(true);
|
|
1070
|
+
redrawEditor();
|
|
1071
|
+
});
|
|
1072
|
+
canvas.addEventListener("pointermove", (event) => {
|
|
1073
|
+
if (event.pointerType === "touch" && activeTouchPointers.has(event.pointerId)) {
|
|
1074
|
+
activeTouchPointers.set(event.pointerId, {
|
|
1075
|
+
x: event.clientX,
|
|
1076
|
+
y: event.clientY
|
|
1077
|
+
});
|
|
1078
|
+
const distance = touchPointerDistance();
|
|
1079
|
+
if (distance !== undefined && pinchStartDistance !== undefined) {
|
|
1080
|
+
event.preventDefault();
|
|
1081
|
+
drawing = false;
|
|
1082
|
+
setZoom(pinchStartZoom * (distance / Math.max(1, pinchStartDistance)), touchPointerCenter());
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
if (!drawing)
|
|
1087
|
+
return;
|
|
1088
|
+
const point = canvasPoint(event);
|
|
1089
|
+
if (tool === "select" && selectionStart) {
|
|
1090
|
+
const x = Math.min(selectionStart.x, point.x);
|
|
1091
|
+
const y = Math.min(selectionStart.y, point.y);
|
|
1092
|
+
selection = {
|
|
1093
|
+
x,
|
|
1094
|
+
y,
|
|
1095
|
+
width: Math.max(1, Math.abs(point.x - selectionStart.x) + 1),
|
|
1096
|
+
height: Math.max(1, Math.abs(point.y - selectionStart.y) + 1)
|
|
1097
|
+
};
|
|
1098
|
+
redrawEditor();
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
if (tool === "brush" || tool === "eraser") {
|
|
1102
|
+
paintAt(point);
|
|
1103
|
+
setDirty(true);
|
|
1104
|
+
redrawEditor();
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
const finishPointer = (event) => {
|
|
1108
|
+
if (event.pointerType === "touch") {
|
|
1109
|
+
activeTouchPointers.delete(event.pointerId);
|
|
1110
|
+
if (activeTouchPointers.size < 2) {
|
|
1111
|
+
pinchStartDistance = undefined;
|
|
1112
|
+
}
|
|
1113
|
+
else {
|
|
1114
|
+
pinchStartDistance = touchPointerDistance();
|
|
1115
|
+
pinchStartZoom = zoom;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
drawing = false;
|
|
1119
|
+
selectionStart = undefined;
|
|
1120
|
+
if (canvas.hasPointerCapture(event.pointerId)) {
|
|
1121
|
+
canvas.releasePointerCapture(event.pointerId);
|
|
1122
|
+
}
|
|
1123
|
+
redrawEditor();
|
|
1124
|
+
};
|
|
1125
|
+
canvas.addEventListener("pointerup", finishPointer);
|
|
1126
|
+
canvas.addEventListener("pointercancel", finishPointer);
|
|
1127
|
+
canvasWrap.addEventListener("wheel", (event) => {
|
|
1128
|
+
if (!event.ctrlKey && !event.metaKey)
|
|
1129
|
+
return;
|
|
1130
|
+
event.preventDefault();
|
|
1131
|
+
const direction = event.deltaY > 0 ? -1 : 1;
|
|
1132
|
+
setZoom(zoom * (direction > 0 ? 1.12 : 1 / 1.12), {
|
|
1133
|
+
clientX: event.clientX,
|
|
1134
|
+
clientY: event.clientY
|
|
1135
|
+
});
|
|
1136
|
+
}, { passive: false });
|
|
1137
|
+
closeButton.addEventListener("click", requestClose);
|
|
1138
|
+
zoomOutButton.addEventListener("click", () => {
|
|
1139
|
+
setZoom(zoom - 1);
|
|
1140
|
+
});
|
|
1141
|
+
zoomInButton.addEventListener("click", () => {
|
|
1142
|
+
setZoom(zoom + 1);
|
|
1143
|
+
});
|
|
1144
|
+
brushButton.addEventListener("click", () => {
|
|
1145
|
+
tool = "brush";
|
|
1146
|
+
updateToolButtons();
|
|
1147
|
+
});
|
|
1148
|
+
brushMenuButton.addEventListener("click", () => {
|
|
1149
|
+
brushMenu.hidden = !brushMenu.hidden;
|
|
1150
|
+
});
|
|
1151
|
+
brushSizeInput.addEventListener("input", () => {
|
|
1152
|
+
brushSizeValue.textContent = brushSizeInput.value;
|
|
1153
|
+
});
|
|
1154
|
+
document.addEventListener("pointerdown", closeBrushMenuOnOutsidePointer);
|
|
1155
|
+
eraserButton.addEventListener("click", () => {
|
|
1156
|
+
tool = "eraser";
|
|
1157
|
+
updateToolButtons();
|
|
1158
|
+
});
|
|
1159
|
+
pickerButton.addEventListener("click", () => {
|
|
1160
|
+
tool = "picker";
|
|
1161
|
+
updateToolButtons();
|
|
1162
|
+
});
|
|
1163
|
+
selectButton.addEventListener("click", () => {
|
|
1164
|
+
tool = "select";
|
|
1165
|
+
updateToolButtons();
|
|
1166
|
+
});
|
|
1167
|
+
fillButton.addEventListener("click", () => {
|
|
1168
|
+
tool = "fill";
|
|
1169
|
+
updateToolButtons();
|
|
1170
|
+
});
|
|
1171
|
+
undoButton.addEventListener("click", () => {
|
|
1172
|
+
const previous = undoStack.pop();
|
|
1173
|
+
if (!previous)
|
|
1174
|
+
return;
|
|
1175
|
+
redoStack.push(snapshot());
|
|
1176
|
+
restore(previous);
|
|
1177
|
+
updateUndoRedo();
|
|
1178
|
+
});
|
|
1179
|
+
redoButton.addEventListener("click", () => {
|
|
1180
|
+
const next = redoStack.pop();
|
|
1181
|
+
if (!next)
|
|
1182
|
+
return;
|
|
1183
|
+
undoStack.push(snapshot());
|
|
1184
|
+
restore(next);
|
|
1185
|
+
updateUndoRedo();
|
|
1186
|
+
});
|
|
1187
|
+
saveButton.addEventListener("click", async () => {
|
|
1188
|
+
await options.onSave(canvas.toDataURL("image/png"));
|
|
1189
|
+
setDirty(false);
|
|
1190
|
+
close();
|
|
1191
|
+
});
|
|
1192
|
+
window.addEventListener("keydown", keyHandler);
|
|
1193
|
+
updateZoom();
|
|
1194
|
+
updateToolButtons();
|
|
1195
|
+
updateUndoRedo();
|
|
1196
|
+
refreshPreviews();
|
|
1197
|
+
drawAnimationFrame();
|
|
1198
|
+
}
|
|
1199
|
+
export function touchUpButton(label) {
|
|
1200
|
+
const button = document.createElement("button");
|
|
1201
|
+
button.type = "button";
|
|
1202
|
+
button.textContent = label;
|
|
1203
|
+
return button;
|
|
1204
|
+
}
|
|
1205
|
+
export function hexByte(value) {
|
|
1206
|
+
return Math.max(0, Math.min(255, value)).toString(16).padStart(2, "0");
|
|
1207
|
+
}
|
|
1208
|
+
export function loadImageElement(src) {
|
|
1209
|
+
return new Promise((resolve, reject) => {
|
|
1210
|
+
const image = new Image();
|
|
1211
|
+
image.onload = () => resolve(image);
|
|
1212
|
+
image.onerror = () => reject(new Error("Could not load image."));
|
|
1213
|
+
image.src = src;
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
export function audioDurationFromDataUrl(src) {
|
|
1217
|
+
return new Promise((resolve) => {
|
|
1218
|
+
const audio = document.createElement("audio");
|
|
1219
|
+
audio.preload = "metadata";
|
|
1220
|
+
audio.addEventListener("loadedmetadata", () => {
|
|
1221
|
+
resolve(Number.isFinite(audio.duration) ? audio.duration : undefined);
|
|
1222
|
+
}, { once: true });
|
|
1223
|
+
audio.addEventListener("error", () => resolve(undefined), { once: true });
|
|
1224
|
+
audio.src = src;
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
export function audioFormatFromMimeType(mimeType, fileName) {
|
|
1228
|
+
if (mimeType.includes("wav") || /\.wav$/i.test(fileName))
|
|
1229
|
+
return "wav";
|
|
1230
|
+
if (mimeType.includes("ogg") || /\.ogg$/i.test(fileName))
|
|
1231
|
+
return "ogg";
|
|
1232
|
+
if (mimeType.includes("opus") || /\.opus$/i.test(fileName))
|
|
1233
|
+
return "opus";
|
|
1234
|
+
if (mimeType.includes("pcm") || /\.pcm$/i.test(fileName))
|
|
1235
|
+
return "pcm";
|
|
1236
|
+
return "mp3";
|
|
1237
|
+
}
|
|
1238
|
+
export function normalizeAssetFormatFromMimeType(mimeType, fileName) {
|
|
1239
|
+
if (mimeType.includes("svg") || /\.svg$/i.test(fileName))
|
|
1240
|
+
return "svg";
|
|
1241
|
+
if (mimeType.includes("webp") || /\.webp$/i.test(fileName))
|
|
1242
|
+
return "webp";
|
|
1243
|
+
if (mimeType.includes("jpeg") || /\.jpe?g$/i.test(fileName))
|
|
1244
|
+
return "jpg";
|
|
1245
|
+
return "png";
|
|
1246
|
+
}
|
|
1247
|
+
export function renderAudioPlayer(options) {
|
|
1248
|
+
resetAudioPlayerContainer(options.container);
|
|
1249
|
+
if (!options.src) {
|
|
1250
|
+
options.container.hidden = true;
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
options.container.hidden = false;
|
|
1254
|
+
const root = document.createElement("div");
|
|
1255
|
+
root.className = "ai-game-assets-designer__audio-player";
|
|
1256
|
+
root.addEventListener("click", (event) => event.stopPropagation());
|
|
1257
|
+
root.addEventListener("keydown", (event) => event.stopPropagation());
|
|
1258
|
+
const audio = document.createElement("audio");
|
|
1259
|
+
audio.src = options.src;
|
|
1260
|
+
audio.preload = "metadata";
|
|
1261
|
+
const playButton = document.createElement("button");
|
|
1262
|
+
playButton.type = "button";
|
|
1263
|
+
playButton.className = "ai-game-assets-designer__audio-play";
|
|
1264
|
+
playButton.setAttribute("aria-label", `Play ${options.label}`);
|
|
1265
|
+
playButton.textContent = "Play";
|
|
1266
|
+
const timeLabel = document.createElement("span");
|
|
1267
|
+
timeLabel.className = "ai-game-assets-designer__audio-time";
|
|
1268
|
+
timeLabel.textContent = "0:00 / 0:00";
|
|
1269
|
+
const trimLabel = document.createElement("span");
|
|
1270
|
+
trimLabel.className = "ai-game-assets-designer__audio-trim";
|
|
1271
|
+
trimLabel.hidden = true;
|
|
1272
|
+
const canvas = document.createElement("canvas");
|
|
1273
|
+
canvas.className = "ai-game-assets-designer__audio-waveform";
|
|
1274
|
+
canvas.width = 420;
|
|
1275
|
+
canvas.height = 72;
|
|
1276
|
+
canvas.setAttribute("aria-label", `${options.label} waveform`);
|
|
1277
|
+
canvas.setAttribute("role", "img");
|
|
1278
|
+
const controls = document.createElement("div");
|
|
1279
|
+
controls.className = "ai-game-assets-designer__audio-controls";
|
|
1280
|
+
controls.append(playButton, timeLabel);
|
|
1281
|
+
root.append(controls, canvas, trimLabel, audio);
|
|
1282
|
+
options.container.append(root);
|
|
1283
|
+
const state = {
|
|
1284
|
+
peaks: undefined,
|
|
1285
|
+
frame: undefined,
|
|
1286
|
+
disposed: false,
|
|
1287
|
+
duration: 0,
|
|
1288
|
+
trimStart: 0,
|
|
1289
|
+
trimEnd: 0
|
|
1290
|
+
};
|
|
1291
|
+
const draw = () => {
|
|
1292
|
+
drawAudioEditorWaveform(canvas, {
|
|
1293
|
+
peaks: state.peaks,
|
|
1294
|
+
duration: state.duration,
|
|
1295
|
+
progress: state.duration > 0 ? audio.currentTime / state.duration : 0,
|
|
1296
|
+
trimStart: state.duration > 0 ? state.trimStart / state.duration : 0,
|
|
1297
|
+
trimEnd: state.duration > 0 ? state.trimEnd / state.duration : 1
|
|
1298
|
+
});
|
|
1299
|
+
};
|
|
1300
|
+
const sync = () => {
|
|
1301
|
+
playButton.textContent = audio.paused ? "Play" : "Pause";
|
|
1302
|
+
playButton.setAttribute("aria-label", `${audio.paused ? "Play" : "Pause"} ${options.label}`);
|
|
1303
|
+
timeLabel.textContent = `${formatAudioTime(audio.currentTime)} / ${formatAudioTime(state.trimEnd || state.duration)}`;
|
|
1304
|
+
draw();
|
|
1305
|
+
};
|
|
1306
|
+
const tick = () => {
|
|
1307
|
+
if (state.disposed)
|
|
1308
|
+
return;
|
|
1309
|
+
if (!audio.paused && state.trimEnd > 0 && audio.currentTime >= state.trimEnd) {
|
|
1310
|
+
audio.currentTime = state.trimStart;
|
|
1311
|
+
if (!options.playback?.loop) {
|
|
1312
|
+
audio.pause();
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
sync();
|
|
1316
|
+
if (!audio.paused) {
|
|
1317
|
+
state.frame = window.requestAnimationFrame(tick);
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
playButton.addEventListener("click", async () => {
|
|
1321
|
+
if (audio.paused) {
|
|
1322
|
+
pauseSiblingAudioPlayers(options.container);
|
|
1323
|
+
audio.volume = clamp(options.playback?.volume ?? 1, 0, 1);
|
|
1324
|
+
audio.playbackRate = clamp(options.playback?.playbackRate ?? 1, 0.5, 2);
|
|
1325
|
+
if (state.trimEnd > 0 &&
|
|
1326
|
+
(audio.currentTime < state.trimStart || audio.currentTime >= state.trimEnd)) {
|
|
1327
|
+
audio.currentTime = state.trimStart;
|
|
1328
|
+
}
|
|
1329
|
+
try {
|
|
1330
|
+
await audio.play();
|
|
1331
|
+
}
|
|
1332
|
+
catch {
|
|
1333
|
+
// Browsers can reject playback when user activation is unavailable.
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
audio.pause();
|
|
1338
|
+
}
|
|
1339
|
+
tick();
|
|
1340
|
+
});
|
|
1341
|
+
canvas.addEventListener("click", (event) => {
|
|
1342
|
+
if (state.duration <= 0)
|
|
1343
|
+
return;
|
|
1344
|
+
const rect = canvas.getBoundingClientRect();
|
|
1345
|
+
const progress = clamp((event.clientX - rect.left) / rect.width, 0, 1);
|
|
1346
|
+
audio.currentTime = seekableAudioTime(state, progress);
|
|
1347
|
+
sync();
|
|
1348
|
+
});
|
|
1349
|
+
audio.addEventListener("loadedmetadata", () => {
|
|
1350
|
+
state.duration = Number.isFinite(audio.duration) ? audio.duration : 0;
|
|
1351
|
+
state.trimStart = clamp(options.playback?.trimStartSeconds ?? 0, 0, state.duration);
|
|
1352
|
+
state.trimEnd = clamp(options.playback?.trimEndSeconds ?? state.duration, state.trimStart, state.duration);
|
|
1353
|
+
trimLabel.hidden = state.trimStart === 0 && state.trimEnd === state.duration;
|
|
1354
|
+
trimLabel.textContent = `Trim ${formatAudioTime(state.trimStart)} – ${formatAudioTime(state.trimEnd)}`;
|
|
1355
|
+
audio.currentTime = state.trimStart;
|
|
1356
|
+
sync();
|
|
1357
|
+
});
|
|
1358
|
+
audio.addEventListener("timeupdate", sync);
|
|
1359
|
+
audio.addEventListener("ended", sync);
|
|
1360
|
+
draw();
|
|
1361
|
+
void audioWaveformPeaks(options.src).then((peaks) => {
|
|
1362
|
+
if (state.disposed)
|
|
1363
|
+
return;
|
|
1364
|
+
state.peaks = peaks;
|
|
1365
|
+
draw();
|
|
1366
|
+
});
|
|
1367
|
+
audio.addEventListener("emptied", () => {
|
|
1368
|
+
state.disposed = true;
|
|
1369
|
+
if (state.frame !== undefined) {
|
|
1370
|
+
window.cancelAnimationFrame(state.frame);
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
export function seekableAudioTime(state, progress) {
|
|
1375
|
+
const start = clamp(state.trimStart, 0, state.duration);
|
|
1376
|
+
const end = clamp(state.trimEnd || state.duration, start, state.duration);
|
|
1377
|
+
return start + ((end - start) * clamp(progress, 0, 1));
|
|
1378
|
+
}
|
|
1379
|
+
export function resetAudioPlayerContainer(container) {
|
|
1380
|
+
for (const audio of container.querySelectorAll("audio")) {
|
|
1381
|
+
audio.pause();
|
|
1382
|
+
audio.removeAttribute("src");
|
|
1383
|
+
audio.load();
|
|
1384
|
+
}
|
|
1385
|
+
container.innerHTML = "";
|
|
1386
|
+
}
|
|
1387
|
+
export function pauseSiblingAudioPlayers(container) {
|
|
1388
|
+
const root = container.closest(".ai-game-assets-designer");
|
|
1389
|
+
for (const audio of root?.querySelectorAll("audio") ?? []) {
|
|
1390
|
+
audio.pause();
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
let sharedAudioContext;
|
|
1394
|
+
export async function audioWaveformPeaks(src, bucketCount = 96) {
|
|
1395
|
+
const response = await fetch(src);
|
|
1396
|
+
const bytes = await response.arrayBuffer();
|
|
1397
|
+
const context = sharedAudioContext ??= new AudioContext();
|
|
1398
|
+
const buffer = await context.decodeAudioData(bytes.slice(0));
|
|
1399
|
+
const channel = buffer.getChannelData(0);
|
|
1400
|
+
const bucketSize = Math.max(1, Math.floor(channel.length / bucketCount));
|
|
1401
|
+
const peaks = [];
|
|
1402
|
+
for (let bucket = 0; bucket < bucketCount; bucket += 1) {
|
|
1403
|
+
const start = bucket * bucketSize;
|
|
1404
|
+
const end = Math.min(channel.length, start + bucketSize);
|
|
1405
|
+
let min = 0;
|
|
1406
|
+
let max = 0;
|
|
1407
|
+
for (let index = start; index < end; index += 1) {
|
|
1408
|
+
const sample = channel[index] ?? 0;
|
|
1409
|
+
min = Math.min(min, sample);
|
|
1410
|
+
max = Math.max(max, sample);
|
|
1411
|
+
}
|
|
1412
|
+
peaks.push({ min, max });
|
|
1413
|
+
}
|
|
1414
|
+
return peaks;
|
|
1415
|
+
}
|
|
1416
|
+
export function drawAudioWaveform(canvas, peaks, progress) {
|
|
1417
|
+
const context = canvas.getContext("2d");
|
|
1418
|
+
if (!context)
|
|
1419
|
+
return;
|
|
1420
|
+
const width = canvas.width;
|
|
1421
|
+
const height = canvas.height;
|
|
1422
|
+
const centerY = height / 2;
|
|
1423
|
+
context.clearRect(0, 0, width, height);
|
|
1424
|
+
context.fillStyle = "#111827";
|
|
1425
|
+
context.fillRect(0, 0, width, height);
|
|
1426
|
+
context.strokeStyle = "rgba(148, 163, 184, 0.26)";
|
|
1427
|
+
context.beginPath();
|
|
1428
|
+
context.moveTo(0, centerY);
|
|
1429
|
+
context.lineTo(width, centerY);
|
|
1430
|
+
context.stroke();
|
|
1431
|
+
const resolvedPeaks = peaks ?? Array.from({ length: 48 }, () => ({ min: -0.06, max: 0.06 }));
|
|
1432
|
+
const barWidth = width / resolvedPeaks.length;
|
|
1433
|
+
const playheadX = clamp(progress, 0, 1) * width;
|
|
1434
|
+
for (const [index, peak] of resolvedPeaks.entries()) {
|
|
1435
|
+
const x = index * barWidth;
|
|
1436
|
+
const minY = centerY + (peak.min * centerY * 0.92);
|
|
1437
|
+
const maxY = centerY + (peak.max * centerY * 0.92);
|
|
1438
|
+
context.fillStyle = x <= playheadX ? "#7dd3fc" : "#64748b";
|
|
1439
|
+
context.fillRect(x + 1, Math.min(minY, maxY), Math.max(1, barWidth - 2), Math.max(2, Math.abs(maxY - minY)));
|
|
1440
|
+
}
|
|
1441
|
+
context.fillStyle = "#ffffff";
|
|
1442
|
+
context.fillRect(playheadX, 6, 2, height - 12);
|
|
1443
|
+
}
|
|
1444
|
+
export function drawAudioEditorWaveform(canvas, options) {
|
|
1445
|
+
drawAudioWaveform(canvas, options.peaks, options.progress);
|
|
1446
|
+
const context = canvas.getContext("2d");
|
|
1447
|
+
if (!context)
|
|
1448
|
+
return;
|
|
1449
|
+
const width = canvas.width;
|
|
1450
|
+
const height = canvas.height;
|
|
1451
|
+
const startX = clamp(options.trimStart, 0, 1) * width;
|
|
1452
|
+
const endX = clamp(options.trimEnd, 0, 1) * width;
|
|
1453
|
+
context.fillStyle = "rgba(2, 6, 23, 0.68)";
|
|
1454
|
+
context.fillRect(0, 0, startX, height);
|
|
1455
|
+
context.fillRect(endX, 0, width - endX, height);
|
|
1456
|
+
context.strokeStyle = "#fbbf24";
|
|
1457
|
+
context.lineWidth = 3;
|
|
1458
|
+
context.beginPath();
|
|
1459
|
+
context.moveTo(startX, 0);
|
|
1460
|
+
context.lineTo(startX, height);
|
|
1461
|
+
context.moveTo(endX, 0);
|
|
1462
|
+
context.lineTo(endX, height);
|
|
1463
|
+
context.stroke();
|
|
1464
|
+
context.fillStyle = "#fbbf24";
|
|
1465
|
+
context.fillRect(startX - 5, 0, 10, 18);
|
|
1466
|
+
context.fillRect(endX - 5, 0, 10, 18);
|
|
1467
|
+
}
|
|
1468
|
+
export function progressForAudio(audio) {
|
|
1469
|
+
if (!Number.isFinite(audio.duration) || audio.duration <= 0) {
|
|
1470
|
+
return 0;
|
|
1471
|
+
}
|
|
1472
|
+
return audio.currentTime / audio.duration;
|
|
1473
|
+
}
|
|
1474
|
+
export function clamp(value, min, max) {
|
|
1475
|
+
return Math.min(max, Math.max(min, value));
|
|
1476
|
+
}
|
|
1477
|
+
export function formatAudioTime(value) {
|
|
1478
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1479
|
+
return "0:00";
|
|
1480
|
+
}
|
|
1481
|
+
const minutes = Math.floor(value / 60);
|
|
1482
|
+
const seconds = Math.floor(value % 60);
|
|
1483
|
+
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
|
1484
|
+
}
|
|
1485
|
+
export function startSpritesheetPreview(options) {
|
|
1486
|
+
const frameGrid = options.asset.frameGrid;
|
|
1487
|
+
if (!frameGrid) {
|
|
1488
|
+
return () => undefined;
|
|
1489
|
+
}
|
|
1490
|
+
const frames = options.asset.animations?.[0]?.frames ??
|
|
1491
|
+
Array.from({ length: frameGrid.frameCount ?? frameGrid.columns * frameGrid.rows }, (_, index) => index);
|
|
1492
|
+
const frameRate = options.asset.animations?.[0]?.frameRate ?? 8;
|
|
1493
|
+
const frameTimings = options.asset.animations?.[0]?.frameTimings ?? [];
|
|
1494
|
+
let frameCursor = 0;
|
|
1495
|
+
let timeout;
|
|
1496
|
+
const frameElement = ensureFramePreviewElement(options.element);
|
|
1497
|
+
const renderFrame = () => {
|
|
1498
|
+
const frame = frames[frameCursor % frames.length] ?? 0;
|
|
1499
|
+
const timing = frameTimings[frameCursor % frames.length];
|
|
1500
|
+
const applyFrameTransforms = options.applyFrameTransforms ?? true;
|
|
1501
|
+
const offsetX = applyFrameTransforms ? timing?.offsetX ?? 0 : 0;
|
|
1502
|
+
const offsetY = applyFrameTransforms ? timing?.offsetY ?? 0 : 0;
|
|
1503
|
+
const scaleX = applyFrameTransforms ? timing?.scaleX ?? 1 : 1;
|
|
1504
|
+
const scaleY = applyFrameTransforms ? timing?.scaleY ?? 1 : 1;
|
|
1505
|
+
const rotation = applyFrameTransforms ? timing?.rotation ?? 0 : 0;
|
|
1506
|
+
options.element.style.width = `${options.displaySize.width}px`;
|
|
1507
|
+
options.element.style.height = `${options.displaySize.height}px`;
|
|
1508
|
+
setFrameElementBackground(frameElement, {
|
|
1509
|
+
src: options.src,
|
|
1510
|
+
frame,
|
|
1511
|
+
frameGrid,
|
|
1512
|
+
displaySize: options.displaySize
|
|
1513
|
+
});
|
|
1514
|
+
frameElement.style.transform =
|
|
1515
|
+
`translate(${offsetX}px, ${offsetY}px) ` +
|
|
1516
|
+
`scale(${scaleX}, ${scaleY}) rotate(${rotation}deg)`;
|
|
1517
|
+
frameElement.style.transformOrigin = "center";
|
|
1518
|
+
frameCursor += 1;
|
|
1519
|
+
timeout = window.setTimeout(renderFrame, timing?.delayMs ?? 1000 / frameRate);
|
|
1520
|
+
};
|
|
1521
|
+
renderFrame();
|
|
1522
|
+
return () => {
|
|
1523
|
+
if (timeout !== undefined) {
|
|
1524
|
+
window.clearTimeout(timeout);
|
|
1525
|
+
}
|
|
1526
|
+
options.element.removeAttribute("style");
|
|
1527
|
+
frameElement.remove();
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
export async function openStyleGuideEditor(options) {
|
|
1531
|
+
const dialog = document.createElement("div");
|
|
1532
|
+
dialog.className = "ai-game-assets-designer__modal";
|
|
1533
|
+
dialog.setAttribute("role", "dialog");
|
|
1534
|
+
dialog.setAttribute("aria-modal", "true");
|
|
1535
|
+
dialog.setAttribute("aria-label", "Define style guide");
|
|
1536
|
+
const card = document.createElement("div");
|
|
1537
|
+
card.className = "ai-game-assets-designer__modal-card";
|
|
1538
|
+
const title = document.createElement("div");
|
|
1539
|
+
title.className = "ai-game-assets-designer__modal-title";
|
|
1540
|
+
title.textContent = "Style guide";
|
|
1541
|
+
const promptInput = document.createElement("textarea");
|
|
1542
|
+
promptInput.rows = 5;
|
|
1543
|
+
promptInput.value = options.initial.prompt;
|
|
1544
|
+
const promptField = labelWrap("Style prompt", promptInput);
|
|
1545
|
+
const fileInput = document.createElement("input");
|
|
1546
|
+
fileInput.type = "file";
|
|
1547
|
+
fileInput.accept = "image/png,image/jpeg,image/webp";
|
|
1548
|
+
fileInput.multiple = true;
|
|
1549
|
+
fileInput.hidden = true;
|
|
1550
|
+
const uploadButton = document.createElement("button");
|
|
1551
|
+
uploadButton.type = "button";
|
|
1552
|
+
uploadButton.textContent = "Upload images";
|
|
1553
|
+
const dropZone = document.createElement("div");
|
|
1554
|
+
dropZone.className = "ai-game-assets-designer__style-drop";
|
|
1555
|
+
dropZone.tabIndex = 0;
|
|
1556
|
+
dropZone.textContent = "Drop style reference images here";
|
|
1557
|
+
const imageList = document.createElement("div");
|
|
1558
|
+
imageList.className = "ai-game-assets-designer__style-images";
|
|
1559
|
+
let images = options.initial.images.map((image) => ({ ...image }));
|
|
1560
|
+
const renderImages = () => {
|
|
1561
|
+
imageList.innerHTML = "";
|
|
1562
|
+
for (const [index, image] of images.entries()) {
|
|
1563
|
+
const item = document.createElement("div");
|
|
1564
|
+
item.className = "ai-game-assets-designer__style-image";
|
|
1565
|
+
const preview = document.createElement("img");
|
|
1566
|
+
preview.src = image.src;
|
|
1567
|
+
preview.alt = image.name;
|
|
1568
|
+
const name = document.createElement("span");
|
|
1569
|
+
name.textContent = image.name;
|
|
1570
|
+
const remove = document.createElement("button");
|
|
1571
|
+
remove.type = "button";
|
|
1572
|
+
remove.setAttribute("aria-label", `Remove ${image.name}`);
|
|
1573
|
+
remove.textContent = "Remove";
|
|
1574
|
+
remove.addEventListener("click", () => {
|
|
1575
|
+
images = images.filter((_, candidateIndex) => candidateIndex !== index);
|
|
1576
|
+
renderImages();
|
|
1577
|
+
});
|
|
1578
|
+
item.append(preview, name, remove);
|
|
1579
|
+
imageList.append(item);
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
const addFiles = async (files) => {
|
|
1583
|
+
const loaded = await Promise.all(Array.from(files)
|
|
1584
|
+
.filter((file) => file.type.startsWith("image/"))
|
|
1585
|
+
.map(async (file) => ({
|
|
1586
|
+
name: file.name,
|
|
1587
|
+
src: await fileToDataUrl(file)
|
|
1588
|
+
})));
|
|
1589
|
+
images.push(...loaded);
|
|
1590
|
+
renderImages();
|
|
1591
|
+
};
|
|
1592
|
+
uploadButton.addEventListener("click", () => fileInput.click());
|
|
1593
|
+
fileInput.addEventListener("change", () => {
|
|
1594
|
+
if (fileInput.files)
|
|
1595
|
+
void addFiles(fileInput.files);
|
|
1596
|
+
fileInput.value = "";
|
|
1597
|
+
});
|
|
1598
|
+
dropZone.addEventListener("dragover", (event) => {
|
|
1599
|
+
event.preventDefault();
|
|
1600
|
+
dropZone.classList.add("is-dragging");
|
|
1601
|
+
});
|
|
1602
|
+
dropZone.addEventListener("dragleave", () => dropZone.classList.remove("is-dragging"));
|
|
1603
|
+
dropZone.addEventListener("drop", (event) => {
|
|
1604
|
+
event.preventDefault();
|
|
1605
|
+
dropZone.classList.remove("is-dragging");
|
|
1606
|
+
if (event.dataTransfer?.files) {
|
|
1607
|
+
void addFiles(event.dataTransfer.files);
|
|
1608
|
+
}
|
|
1609
|
+
});
|
|
1610
|
+
const cancelButton = document.createElement("button");
|
|
1611
|
+
cancelButton.type = "button";
|
|
1612
|
+
cancelButton.textContent = "Cancel";
|
|
1613
|
+
const confirmButton = document.createElement("button");
|
|
1614
|
+
confirmButton.type = "button";
|
|
1615
|
+
confirmButton.textContent = "Confirm";
|
|
1616
|
+
const promoteButton = document.createElement("button");
|
|
1617
|
+
promoteButton.type = "button";
|
|
1618
|
+
promoteButton.textContent = "Promote style";
|
|
1619
|
+
const actions = document.createElement("div");
|
|
1620
|
+
actions.className = "ai-game-assets-designer__modal-actions";
|
|
1621
|
+
actions.append(cancelButton, promoteButton, confirmButton);
|
|
1622
|
+
const draft = () => ({
|
|
1623
|
+
prompt: promptInput.value.trim(),
|
|
1624
|
+
images: images.map((image) => ({ ...image }))
|
|
1625
|
+
});
|
|
1626
|
+
const close = () => dialog.remove();
|
|
1627
|
+
cancelButton.addEventListener("click", close);
|
|
1628
|
+
confirmButton.addEventListener("click", () => {
|
|
1629
|
+
options.onConfirm(draft());
|
|
1630
|
+
close();
|
|
1631
|
+
});
|
|
1632
|
+
promoteButton.addEventListener("click", async () => {
|
|
1633
|
+
promoteButton.disabled = true;
|
|
1634
|
+
try {
|
|
1635
|
+
const current = draft();
|
|
1636
|
+
options.onConfirm(current);
|
|
1637
|
+
await options.onPromote(current);
|
|
1638
|
+
close();
|
|
1639
|
+
}
|
|
1640
|
+
catch {
|
|
1641
|
+
// The caller reports promotion errors in the designer status area.
|
|
1642
|
+
}
|
|
1643
|
+
finally {
|
|
1644
|
+
promoteButton.disabled = false;
|
|
1645
|
+
}
|
|
1646
|
+
});
|
|
1647
|
+
card.append(title, promptField, dropZone, uploadButton, fileInput, imageList, actions);
|
|
1648
|
+
dialog.append(card);
|
|
1649
|
+
options.root.append(dialog);
|
|
1650
|
+
renderImages();
|
|
1651
|
+
promptInput.focus();
|
|
1652
|
+
}
|
|
1653
|
+
export async function openAudioEditor(options) {
|
|
1654
|
+
const dialog = document.createElement("div");
|
|
1655
|
+
dialog.className = "ai-game-assets-designer__modal";
|
|
1656
|
+
dialog.setAttribute("role", "dialog");
|
|
1657
|
+
dialog.setAttribute("aria-modal", "true");
|
|
1658
|
+
dialog.setAttribute("aria-label", `Edit ${readableAssetName(options.assetId)} sound`);
|
|
1659
|
+
const card = document.createElement("div");
|
|
1660
|
+
card.className = "ai-game-assets-designer__modal-card";
|
|
1661
|
+
const title = document.createElement("div");
|
|
1662
|
+
title.className = "ai-game-assets-designer__modal-title";
|
|
1663
|
+
title.textContent = `Edit ${readableAssetName(options.assetId)}`;
|
|
1664
|
+
const audio = document.createElement("audio");
|
|
1665
|
+
audio.src = options.src;
|
|
1666
|
+
audio.preload = "auto";
|
|
1667
|
+
const stage = document.createElement("div");
|
|
1668
|
+
stage.className = "ai-game-assets-designer__audio-editor-stage";
|
|
1669
|
+
const canvas = document.createElement("canvas");
|
|
1670
|
+
canvas.className = "ai-game-assets-designer__audio-editor-waveform";
|
|
1671
|
+
canvas.width = 720;
|
|
1672
|
+
canvas.height = 160;
|
|
1673
|
+
stage.append(canvas);
|
|
1674
|
+
const playButton = document.createElement("button");
|
|
1675
|
+
playButton.type = "button";
|
|
1676
|
+
playButton.className = "ai-game-assets-designer__audio-editor-play";
|
|
1677
|
+
playButton.setAttribute("aria-label", "Play");
|
|
1678
|
+
playButton.textContent = "▶";
|
|
1679
|
+
const loopInput = document.createElement("input");
|
|
1680
|
+
loopInput.type = "checkbox";
|
|
1681
|
+
const timeLabel = document.createElement("span");
|
|
1682
|
+
timeLabel.className = "ai-game-assets-designer__audio-editor-time";
|
|
1683
|
+
timeLabel.textContent = "0:00 / 0:00";
|
|
1684
|
+
const loopField = inlineCheckboxField("Loop", loopInput);
|
|
1685
|
+
const transport = document.createElement("div");
|
|
1686
|
+
transport.className = "ai-game-assets-designer__audio-editor-transport";
|
|
1687
|
+
transport.append(playButton, loopField, timeLabel);
|
|
1688
|
+
const seekInput = rangeInput(0, 1, 0.001);
|
|
1689
|
+
seekInput.className = "ai-game-assets-designer__audio-editor-scrubber";
|
|
1690
|
+
stage.append(seekInput);
|
|
1691
|
+
const volumeInput = rangeInput(0, 1.5, 0.01);
|
|
1692
|
+
const speedInput = rangeInput(0.5, 2, 0.01);
|
|
1693
|
+
const fields = document.createElement("div");
|
|
1694
|
+
fields.className = "ai-game-assets-designer__audio-editor-fields";
|
|
1695
|
+
fields.append(labelWrap("Volume", volumeInput), labelWrap("Speed", speedInput));
|
|
1696
|
+
const hint = document.createElement("div");
|
|
1697
|
+
hint.className = "ai-game-assets-designer__audio-editor-hint";
|
|
1698
|
+
hint.textContent = "Drag the start and end markers on the waveform to trim the playable region.";
|
|
1699
|
+
const cancelButton = document.createElement("button");
|
|
1700
|
+
cancelButton.type = "button";
|
|
1701
|
+
cancelButton.textContent = "Cancel";
|
|
1702
|
+
const confirmButton = document.createElement("button");
|
|
1703
|
+
confirmButton.type = "button";
|
|
1704
|
+
confirmButton.textContent = "Confirm";
|
|
1705
|
+
const actions = document.createElement("div");
|
|
1706
|
+
actions.className = "ai-game-assets-designer__modal-actions";
|
|
1707
|
+
actions.append(cancelButton, confirmButton);
|
|
1708
|
+
card.append(title, stage, transport, fields, hint, actions, audio);
|
|
1709
|
+
dialog.append(card);
|
|
1710
|
+
options.root.append(dialog);
|
|
1711
|
+
const initial = options.initialPlayback ?? {};
|
|
1712
|
+
const hasInitialTrimStart = initial.trimStartSeconds !== undefined;
|
|
1713
|
+
const hasInitialTrimEnd = initial.trimEndSeconds !== undefined;
|
|
1714
|
+
let duration = Math.max(0, options.asset.audioSettings?.durationSeconds ?? 0);
|
|
1715
|
+
let trimStart = hasInitialTrimStart ? Math.max(0, initial.trimStartSeconds ?? 0) : 0;
|
|
1716
|
+
let trimEnd = hasInitialTrimEnd ? Math.max(trimStart, initial.trimEndSeconds ?? duration) : duration;
|
|
1717
|
+
let peaks;
|
|
1718
|
+
let dragTarget;
|
|
1719
|
+
let animationFrame;
|
|
1720
|
+
volumeInput.value = String(clamp(initial.volume ?? 1, 0, 1.5));
|
|
1721
|
+
speedInput.value = String(clamp(initial.playbackRate ?? 1, 0.5, 2));
|
|
1722
|
+
loopInput.checked = Boolean(initial.loop);
|
|
1723
|
+
const playback = () => ({
|
|
1724
|
+
volume: numberInput(volumeInput, 1),
|
|
1725
|
+
trimStartSeconds: trimStart,
|
|
1726
|
+
trimEndSeconds: trimEnd,
|
|
1727
|
+
playbackRate: numberInput(speedInput, 1),
|
|
1728
|
+
loop: loopInput.checked || undefined
|
|
1729
|
+
});
|
|
1730
|
+
const syncAudioSettings = () => {
|
|
1731
|
+
audio.volume = clamp(numberInput(volumeInput, 1), 0, 1);
|
|
1732
|
+
audio.playbackRate = clamp(numberInput(speedInput, 1), 0.5, 2);
|
|
1733
|
+
};
|
|
1734
|
+
const seekTo = (value) => {
|
|
1735
|
+
audio.currentTime = clamp(value, trimStart, trimEnd || duration);
|
|
1736
|
+
};
|
|
1737
|
+
const draw = () => {
|
|
1738
|
+
drawAudioEditorWaveform(canvas, {
|
|
1739
|
+
peaks,
|
|
1740
|
+
duration,
|
|
1741
|
+
progress: duration > 0 ? audio.currentTime / duration : 0,
|
|
1742
|
+
trimStart: duration > 0 ? trimStart / duration : 0,
|
|
1743
|
+
trimEnd: duration > 0 ? trimEnd / duration : 1
|
|
1744
|
+
});
|
|
1745
|
+
timeLabel.textContent = `${formatAudioTime(audio.currentTime)} / ${formatAudioTime(duration)}`;
|
|
1746
|
+
seekInput.value = String(trimEnd > trimStart ? (audio.currentTime - trimStart) / (trimEnd - trimStart) : 0);
|
|
1747
|
+
playButton.textContent = audio.paused ? "▶" : "❚❚";
|
|
1748
|
+
playButton.setAttribute("aria-label", audio.paused ? "Play" : "Pause");
|
|
1749
|
+
};
|
|
1750
|
+
const tick = () => {
|
|
1751
|
+
if (!audio.paused && audio.currentTime >= trimEnd) {
|
|
1752
|
+
audio.currentTime = trimStart;
|
|
1753
|
+
if (!loopInput.checked) {
|
|
1754
|
+
audio.pause();
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
draw();
|
|
1758
|
+
animationFrame = window.requestAnimationFrame(tick);
|
|
1759
|
+
};
|
|
1760
|
+
const stopTick = () => {
|
|
1761
|
+
if (animationFrame !== undefined) {
|
|
1762
|
+
window.cancelAnimationFrame(animationFrame);
|
|
1763
|
+
animationFrame = undefined;
|
|
1764
|
+
}
|
|
1765
|
+
};
|
|
1766
|
+
audio.addEventListener("loadedmetadata", () => {
|
|
1767
|
+
duration = Number.isFinite(audio.duration) ? audio.duration : duration;
|
|
1768
|
+
trimStart = hasInitialTrimStart ? clamp(trimStart, 0, duration) : 0;
|
|
1769
|
+
trimEnd = hasInitialTrimEnd ? clamp(trimEnd, trimStart, duration) : duration;
|
|
1770
|
+
seekInput.max = "1";
|
|
1771
|
+
seekTo(trimStart);
|
|
1772
|
+
draw();
|
|
1773
|
+
});
|
|
1774
|
+
audio.addEventListener("timeupdate", draw);
|
|
1775
|
+
for (const input of [volumeInput, speedInput]) {
|
|
1776
|
+
input.addEventListener("input", syncAudioSettings);
|
|
1777
|
+
}
|
|
1778
|
+
seekInput.addEventListener("input", () => {
|
|
1779
|
+
if (duration <= 0)
|
|
1780
|
+
return;
|
|
1781
|
+
seekTo(seekableAudioTime({ duration, trimStart, trimEnd }, Number(seekInput.value)));
|
|
1782
|
+
draw();
|
|
1783
|
+
});
|
|
1784
|
+
seekInput.addEventListener("change", () => {
|
|
1785
|
+
if (duration <= 0)
|
|
1786
|
+
return;
|
|
1787
|
+
seekTo(seekableAudioTime({ duration, trimStart, trimEnd }, Number(seekInput.value)));
|
|
1788
|
+
draw();
|
|
1789
|
+
});
|
|
1790
|
+
playButton.addEventListener("click", async () => {
|
|
1791
|
+
syncAudioSettings();
|
|
1792
|
+
if (audio.paused) {
|
|
1793
|
+
if (audio.currentTime < trimStart || audio.currentTime >= trimEnd || audio.currentTime === 0) {
|
|
1794
|
+
audio.currentTime = trimStart;
|
|
1795
|
+
}
|
|
1796
|
+
try {
|
|
1797
|
+
await audio.play();
|
|
1798
|
+
}
|
|
1799
|
+
catch {
|
|
1800
|
+
// Browser autoplay policy can reject playback if the click activation is lost.
|
|
1801
|
+
}
|
|
1802
|
+
tick();
|
|
1803
|
+
}
|
|
1804
|
+
else {
|
|
1805
|
+
audio.pause();
|
|
1806
|
+
draw();
|
|
1807
|
+
}
|
|
1808
|
+
});
|
|
1809
|
+
cancelButton.addEventListener("click", () => {
|
|
1810
|
+
audio.pause();
|
|
1811
|
+
stopTick();
|
|
1812
|
+
dialog.remove();
|
|
1813
|
+
});
|
|
1814
|
+
confirmButton.addEventListener("click", async () => {
|
|
1815
|
+
audio.pause();
|
|
1816
|
+
stopTick();
|
|
1817
|
+
await options.onConfirm(playback());
|
|
1818
|
+
dialog.remove();
|
|
1819
|
+
});
|
|
1820
|
+
const positionToSeconds = (clientX) => {
|
|
1821
|
+
const rect = canvas.getBoundingClientRect();
|
|
1822
|
+
return clamp(((clientX - rect.left) / rect.width) * duration, 0, duration);
|
|
1823
|
+
};
|
|
1824
|
+
canvas.addEventListener("pointerdown", (event) => {
|
|
1825
|
+
if (duration <= 0)
|
|
1826
|
+
return;
|
|
1827
|
+
const seconds = positionToSeconds(event.clientX);
|
|
1828
|
+
const startDistance = Math.abs(seconds - trimStart);
|
|
1829
|
+
const endDistance = Math.abs(seconds - trimEnd);
|
|
1830
|
+
dragTarget = startDistance < 0.12 || startDistance < endDistance ? "start" : "end";
|
|
1831
|
+
canvas.setPointerCapture(event.pointerId);
|
|
1832
|
+
});
|
|
1833
|
+
canvas.addEventListener("pointermove", (event) => {
|
|
1834
|
+
if (!dragTarget || duration <= 0)
|
|
1835
|
+
return;
|
|
1836
|
+
const seconds = positionToSeconds(event.clientX);
|
|
1837
|
+
if (dragTarget === "start") {
|
|
1838
|
+
trimStart = clamp(seconds, 0, Math.max(0, trimEnd - 0.03));
|
|
1839
|
+
if (audio.currentTime < trimStart || audio.currentTime === 0)
|
|
1840
|
+
audio.currentTime = trimStart;
|
|
1841
|
+
}
|
|
1842
|
+
else {
|
|
1843
|
+
trimEnd = clamp(seconds, Math.min(duration, trimStart + 0.03), duration);
|
|
1844
|
+
if (audio.currentTime > trimEnd)
|
|
1845
|
+
audio.currentTime = trimEnd;
|
|
1846
|
+
}
|
|
1847
|
+
draw();
|
|
1848
|
+
});
|
|
1849
|
+
canvas.addEventListener("pointerup", (event) => {
|
|
1850
|
+
dragTarget = undefined;
|
|
1851
|
+
canvas.releasePointerCapture(event.pointerId);
|
|
1852
|
+
});
|
|
1853
|
+
syncAudioSettings();
|
|
1854
|
+
draw();
|
|
1855
|
+
void audioWaveformPeaks(options.src, 160).then((decodedPeaks) => {
|
|
1856
|
+
peaks = decodedPeaks;
|
|
1857
|
+
draw();
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
export async function openAnimationEditor(options) {
|
|
1861
|
+
const frameGrid = options.asset.frameGrid;
|
|
1862
|
+
const baseAnimation = options.initialAnimations?.[0] ?? options.asset.animations?.[0];
|
|
1863
|
+
if (!frameGrid || !baseAnimation)
|
|
1864
|
+
return;
|
|
1865
|
+
const dialog = document.createElement("div");
|
|
1866
|
+
dialog.className = "ai-game-assets-designer__modal";
|
|
1867
|
+
dialog.setAttribute("role", "dialog");
|
|
1868
|
+
dialog.setAttribute("aria-modal", "true");
|
|
1869
|
+
dialog.setAttribute("aria-label", `Edit ${readableAssetName(options.assetId)} animation`);
|
|
1870
|
+
const card = document.createElement("div");
|
|
1871
|
+
card.className = "ai-game-assets-designer__modal-card";
|
|
1872
|
+
const title = document.createElement("div");
|
|
1873
|
+
title.className = "ai-game-assets-designer__modal-title";
|
|
1874
|
+
title.textContent = `Edit ${readableAssetName(options.assetId)}`;
|
|
1875
|
+
const stage = document.createElement("div");
|
|
1876
|
+
stage.className = "ai-game-assets-designer__modal-stage";
|
|
1877
|
+
const strip = document.createElement("div");
|
|
1878
|
+
strip.className = "ai-game-assets-designer__frame-strip";
|
|
1879
|
+
const delayInput = numericInput();
|
|
1880
|
+
delayInput.min = "1";
|
|
1881
|
+
const offsetXInput = signedNumberInput();
|
|
1882
|
+
const offsetYInput = signedNumberInput();
|
|
1883
|
+
const scaleXInput = decimalInput();
|
|
1884
|
+
const scaleYInput = decimalInput();
|
|
1885
|
+
const rotationInput = signedNumberInput();
|
|
1886
|
+
const tagInput = document.createElement("input");
|
|
1887
|
+
tagInput.type = "text";
|
|
1888
|
+
tagInput.placeholder = "shoot";
|
|
1889
|
+
const fields = document.createElement("div");
|
|
1890
|
+
fields.className = "ai-game-assets-designer__frame-fields";
|
|
1891
|
+
fields.append(labelWrap("Delay ms", delayInput), labelWrap("Offset X", offsetXInput), labelWrap("Offset Y", offsetYInput), labelWrap("Scale X", scaleXInput), labelWrap("Scale Y", scaleYInput), labelWrap("Rotation", rotationInput), labelWrap("Tag", tagInput));
|
|
1892
|
+
const cancelButton = document.createElement("button");
|
|
1893
|
+
cancelButton.type = "button";
|
|
1894
|
+
cancelButton.textContent = "Cancel";
|
|
1895
|
+
const uploadFrameButton = document.createElement("button");
|
|
1896
|
+
uploadFrameButton.type = "button";
|
|
1897
|
+
uploadFrameButton.textContent = "Upload frame...";
|
|
1898
|
+
const touchUpFrameButton = document.createElement("button");
|
|
1899
|
+
touchUpFrameButton.type = "button";
|
|
1900
|
+
touchUpFrameButton.textContent = "Touch up...";
|
|
1901
|
+
const confirmButton = document.createElement("button");
|
|
1902
|
+
confirmButton.type = "button";
|
|
1903
|
+
confirmButton.textContent = "Confirm";
|
|
1904
|
+
const actions = document.createElement("div");
|
|
1905
|
+
actions.className = "ai-game-assets-designer__modal-actions";
|
|
1906
|
+
actions.append(uploadFrameButton, touchUpFrameButton, cancelButton, confirmButton);
|
|
1907
|
+
card.append(title, stage, strip, fields, actions);
|
|
1908
|
+
dialog.append(card);
|
|
1909
|
+
options.root.append(dialog);
|
|
1910
|
+
let anchorFrameSlot = 0;
|
|
1911
|
+
const selectedFrameSlots = new Set([0]);
|
|
1912
|
+
let stopPreview;
|
|
1913
|
+
let spriteSheetSrc = options.src;
|
|
1914
|
+
let editedSpriteSheetSrc;
|
|
1915
|
+
const frameTimings = baseAnimation.frames.map((_, index) => ({
|
|
1916
|
+
...baseAnimation.frameTimings?.[index]
|
|
1917
|
+
}));
|
|
1918
|
+
const animationFromTimings = () => ({
|
|
1919
|
+
...baseAnimation,
|
|
1920
|
+
frameTimings: frameTimings.map((timing) => ({
|
|
1921
|
+
delayMs: positiveIntegerValue(timing.delayMs, Math.round(1000 / baseAnimation.frameRate)),
|
|
1922
|
+
offsetX: integerValue(timing.offsetX, 0),
|
|
1923
|
+
offsetY: integerValue(timing.offsetY, 0),
|
|
1924
|
+
scaleX: numberValue(timing.scaleX, 1),
|
|
1925
|
+
scaleY: numberValue(timing.scaleY, 1),
|
|
1926
|
+
rotation: numberValue(timing.rotation, 0),
|
|
1927
|
+
tag: timing.tag?.trim() || undefined
|
|
1928
|
+
}))
|
|
1929
|
+
});
|
|
1930
|
+
const previewAsset = () => ({
|
|
1931
|
+
...options.asset,
|
|
1932
|
+
animations: [animationFromTimings()]
|
|
1933
|
+
});
|
|
1934
|
+
const restartPreview = () => {
|
|
1935
|
+
stopPreview?.();
|
|
1936
|
+
stopPreview = startSpritesheetPreview({
|
|
1937
|
+
element: stage,
|
|
1938
|
+
src: spriteSheetSrc,
|
|
1939
|
+
asset: previewAsset(),
|
|
1940
|
+
displaySize: options.displaySize
|
|
1941
|
+
});
|
|
1942
|
+
};
|
|
1943
|
+
const syncInputs = () => {
|
|
1944
|
+
const selectedTimings = selectedFrameSlotsArray().map((frameSlot) => frameTimings[frameSlot] ?? {});
|
|
1945
|
+
setInputForSelected(delayInput, selectedTimings, (timing) => positiveIntegerValue(timing.delayMs, Math.round(1000 / baseAnimation.frameRate)));
|
|
1946
|
+
setInputForSelected(offsetXInput, selectedTimings, (timing) => integerValue(timing.offsetX, 0));
|
|
1947
|
+
setInputForSelected(offsetYInput, selectedTimings, (timing) => integerValue(timing.offsetY, 0));
|
|
1948
|
+
setInputForSelected(scaleXInput, selectedTimings, (timing) => numberValue(timing.scaleX, 1));
|
|
1949
|
+
setInputForSelected(scaleYInput, selectedTimings, (timing) => numberValue(timing.scaleY, 1));
|
|
1950
|
+
setInputForSelected(rotationInput, selectedTimings, (timing) => numberValue(timing.rotation, 0));
|
|
1951
|
+
setInputForSelected(tagInput, selectedTimings, (timing) => timing.tag ?? "");
|
|
1952
|
+
for (const button of strip.querySelectorAll("button")) {
|
|
1953
|
+
const frameSlot = Number(button.dataset.frameSlot);
|
|
1954
|
+
button.classList.toggle("is-selected", selectedFrameSlots.has(frameSlot));
|
|
1955
|
+
}
|
|
1956
|
+
touchUpFrameButton.disabled = selectedFrameSlots.size !== 1 ||
|
|
1957
|
+
normalizeAssetFormat(options.asset.settings?.format) === "svg" ||
|
|
1958
|
+
isSvgSource(spriteSheetSrc);
|
|
1959
|
+
};
|
|
1960
|
+
const selectedFrameSlotsArray = () => [...selectedFrameSlots].sort((left, right) => left - right);
|
|
1961
|
+
const selectFrame = (index, event) => {
|
|
1962
|
+
if (event.shiftKey) {
|
|
1963
|
+
const start = Math.min(anchorFrameSlot, index);
|
|
1964
|
+
const end = Math.max(anchorFrameSlot, index);
|
|
1965
|
+
selectedFrameSlots.clear();
|
|
1966
|
+
for (let frameSlot = start; frameSlot <= end; frameSlot += 1) {
|
|
1967
|
+
selectedFrameSlots.add(frameSlot);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
else if (event.metaKey || event.ctrlKey) {
|
|
1971
|
+
if (selectedFrameSlots.has(index)) {
|
|
1972
|
+
selectedFrameSlots.delete(index);
|
|
1973
|
+
}
|
|
1974
|
+
else {
|
|
1975
|
+
selectedFrameSlots.add(index);
|
|
1976
|
+
}
|
|
1977
|
+
anchorFrameSlot = index;
|
|
1978
|
+
}
|
|
1979
|
+
else {
|
|
1980
|
+
selectedFrameSlots.clear();
|
|
1981
|
+
selectedFrameSlots.add(index);
|
|
1982
|
+
anchorFrameSlot = index;
|
|
1983
|
+
}
|
|
1984
|
+
syncInputs();
|
|
1985
|
+
};
|
|
1986
|
+
const updateSelectedFrameThumbs = () => {
|
|
1987
|
+
for (const frameSlot of selectedFrameSlots) {
|
|
1988
|
+
const selectedButton = strip.querySelector(`button[data-frame-slot="${frameSlot}"]`);
|
|
1989
|
+
if (selectedButton) {
|
|
1990
|
+
setFrameBackground(selectedButton, {
|
|
1991
|
+
src: spriteSheetSrc,
|
|
1992
|
+
frame: baseAnimation.frames[frameSlot] ?? 0,
|
|
1993
|
+
frameGrid,
|
|
1994
|
+
displaySize: { width: 48, height: 48 },
|
|
1995
|
+
timing: frameTimings[frameSlot]
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
const updateSelectedTimings = (applyTiming, options = {}) => {
|
|
2001
|
+
for (const frameSlot of selectedFrameSlots) {
|
|
2002
|
+
frameTimings[frameSlot] = applyTiming(frameTimings[frameSlot] ?? {});
|
|
2003
|
+
}
|
|
2004
|
+
updateSelectedFrameThumbs();
|
|
2005
|
+
if (options.syncInputsAfterUpdate) {
|
|
2006
|
+
syncInputs();
|
|
2007
|
+
}
|
|
2008
|
+
restartPreview();
|
|
2009
|
+
};
|
|
2010
|
+
const updateSelectedDelay = () => {
|
|
2011
|
+
updateSelectedTimings((timing) => ({
|
|
2012
|
+
...timing,
|
|
2013
|
+
delayMs: positiveIntegerInput(delayInput, Math.round(1000 / baseAnimation.frameRate))
|
|
2014
|
+
}));
|
|
2015
|
+
};
|
|
2016
|
+
const updateSelectedOffsetX = () => {
|
|
2017
|
+
updateSelectedTimings((timing) => ({
|
|
2018
|
+
...timing,
|
|
2019
|
+
offsetX: integerInput(offsetXInput, 0)
|
|
2020
|
+
}));
|
|
2021
|
+
};
|
|
2022
|
+
const updateSelectedOffsetY = () => {
|
|
2023
|
+
updateSelectedTimings((timing) => ({
|
|
2024
|
+
...timing,
|
|
2025
|
+
offsetY: integerInput(offsetYInput, 0)
|
|
2026
|
+
}));
|
|
2027
|
+
};
|
|
2028
|
+
const updateSelectedScaleX = () => {
|
|
2029
|
+
updateSelectedTimings((timing) => ({
|
|
2030
|
+
...timing,
|
|
2031
|
+
scaleX: numberInput(scaleXInput, 1)
|
|
2032
|
+
}));
|
|
2033
|
+
};
|
|
2034
|
+
const updateSelectedScaleY = () => {
|
|
2035
|
+
updateSelectedTimings((timing) => ({
|
|
2036
|
+
...timing,
|
|
2037
|
+
scaleY: numberInput(scaleYInput, 1)
|
|
2038
|
+
}));
|
|
2039
|
+
};
|
|
2040
|
+
const updateSelectedRotation = () => {
|
|
2041
|
+
updateSelectedTimings((timing) => ({
|
|
2042
|
+
...timing,
|
|
2043
|
+
rotation: numberInput(rotationInput, 0)
|
|
2044
|
+
}));
|
|
2045
|
+
};
|
|
2046
|
+
const updateSelectedTag = () => {
|
|
2047
|
+
updateSelectedTimings((timing) => ({
|
|
2048
|
+
...timing,
|
|
2049
|
+
tag: tagInput.value.trim() || undefined
|
|
2050
|
+
}));
|
|
2051
|
+
};
|
|
2052
|
+
const bindFrameInput = (input, update) => {
|
|
2053
|
+
input.addEventListener("input", () => {
|
|
2054
|
+
if (selectedFrameSlots.size === 0)
|
|
2055
|
+
return;
|
|
2056
|
+
update();
|
|
2057
|
+
});
|
|
2058
|
+
};
|
|
2059
|
+
const setInputForSelected = (input, timings, valueForTiming) => {
|
|
2060
|
+
if (timings.length === 0) {
|
|
2061
|
+
input.value = "";
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
const values = timings.map((timing) => String(valueForTiming(timing)));
|
|
2065
|
+
input.value = values.every((value) => value === values[0]) ? values[0] : "";
|
|
2066
|
+
};
|
|
2067
|
+
const renderStrip = () => {
|
|
2068
|
+
strip.innerHTML = "";
|
|
2069
|
+
baseAnimation.frames.forEach((frame, index) => {
|
|
2070
|
+
const button = document.createElement("button");
|
|
2071
|
+
button.type = "button";
|
|
2072
|
+
button.dataset.frameSlot = String(index);
|
|
2073
|
+
button.className = "ai-game-assets-designer__frame-thumb";
|
|
2074
|
+
button.setAttribute("aria-label", `Frame ${index + 1}`);
|
|
2075
|
+
setFrameBackground(button, {
|
|
2076
|
+
src: spriteSheetSrc,
|
|
2077
|
+
frame,
|
|
2078
|
+
frameGrid,
|
|
2079
|
+
displaySize: { width: 48, height: 48 },
|
|
2080
|
+
timing: frameTimings[index]
|
|
2081
|
+
});
|
|
2082
|
+
button.addEventListener("click", (event) => {
|
|
2083
|
+
selectFrame(index, event);
|
|
2084
|
+
});
|
|
2085
|
+
strip.append(button);
|
|
2086
|
+
});
|
|
2087
|
+
};
|
|
2088
|
+
bindFrameInput(delayInput, updateSelectedDelay);
|
|
2089
|
+
bindFrameInput(offsetXInput, updateSelectedOffsetX);
|
|
2090
|
+
bindFrameInput(offsetYInput, updateSelectedOffsetY);
|
|
2091
|
+
bindFrameInput(scaleXInput, updateSelectedScaleX);
|
|
2092
|
+
bindFrameInput(scaleYInput, updateSelectedScaleY);
|
|
2093
|
+
bindFrameInput(rotationInput, updateSelectedRotation);
|
|
2094
|
+
bindFrameInput(tagInput, updateSelectedTag);
|
|
2095
|
+
uploadFrameButton.addEventListener("click", async () => {
|
|
2096
|
+
if (selectedFrameSlots.size === 0)
|
|
2097
|
+
return;
|
|
2098
|
+
const file = await pickUploadFile("image/*");
|
|
2099
|
+
if (!file)
|
|
2100
|
+
return;
|
|
2101
|
+
spriteSheetSrc = await replaceSpriteSheetFrames({
|
|
2102
|
+
src: spriteSheetSrc,
|
|
2103
|
+
uploadSrc: await fileToDataUrl(file),
|
|
2104
|
+
frameGrid,
|
|
2105
|
+
frames: selectedFrameSlotsArray().map((frameSlot) => baseAnimation.frames[frameSlot] ?? 0)
|
|
2106
|
+
});
|
|
2107
|
+
editedSpriteSheetSrc = spriteSheetSrc;
|
|
2108
|
+
renderStrip();
|
|
2109
|
+
syncInputs();
|
|
2110
|
+
restartPreview();
|
|
2111
|
+
});
|
|
2112
|
+
touchUpFrameButton.addEventListener("click", async () => {
|
|
2113
|
+
if (touchUpFrameButton.disabled || selectedFrameSlots.size !== 1)
|
|
2114
|
+
return;
|
|
2115
|
+
const [frameSlot] = selectedFrameSlotsArray();
|
|
2116
|
+
const frame = baseAnimation.frames[frameSlot] ?? 0;
|
|
2117
|
+
const frameSrc = await spriteSheetFrameToDataUrl({
|
|
2118
|
+
src: spriteSheetSrc,
|
|
2119
|
+
frameGrid,
|
|
2120
|
+
frame
|
|
2121
|
+
});
|
|
2122
|
+
await openFrameTouchUpEditor({
|
|
2123
|
+
root: options.root,
|
|
2124
|
+
asset: previewAsset(),
|
|
2125
|
+
title: `${readableAssetName(options.assetId)} frame ${frameSlot + 1}`,
|
|
2126
|
+
frameSrc,
|
|
2127
|
+
spriteSheetSrc,
|
|
2128
|
+
frameSlot,
|
|
2129
|
+
frame,
|
|
2130
|
+
displaySize: options.displaySize,
|
|
2131
|
+
onSave: async (editedFrameSrc) => {
|
|
2132
|
+
spriteSheetSrc = await replaceSpriteSheetFrames({
|
|
2133
|
+
src: spriteSheetSrc,
|
|
2134
|
+
uploadSrc: editedFrameSrc,
|
|
2135
|
+
frameGrid,
|
|
2136
|
+
frames: [frame]
|
|
2137
|
+
});
|
|
2138
|
+
editedSpriteSheetSrc = spriteSheetSrc;
|
|
2139
|
+
renderStrip();
|
|
2140
|
+
syncInputs();
|
|
2141
|
+
restartPreview();
|
|
2142
|
+
}
|
|
2143
|
+
});
|
|
2144
|
+
});
|
|
2145
|
+
const close = () => {
|
|
2146
|
+
stopPreview?.();
|
|
2147
|
+
dialog.remove();
|
|
2148
|
+
};
|
|
2149
|
+
cancelButton.addEventListener("click", close);
|
|
2150
|
+
confirmButton.addEventListener("click", async () => {
|
|
2151
|
+
await options.onConfirm({
|
|
2152
|
+
animations: [animationFromTimings()],
|
|
2153
|
+
dataUrl: editedSpriteSheetSrc
|
|
2154
|
+
});
|
|
2155
|
+
close();
|
|
2156
|
+
});
|
|
2157
|
+
renderStrip();
|
|
2158
|
+
syncInputs();
|
|
2159
|
+
restartPreview();
|
|
2160
|
+
}
|
|
2161
|
+
export function setFrameBackground(element, options) {
|
|
2162
|
+
const frameElement = ensureFramePreviewElement(element);
|
|
2163
|
+
const offsetX = options.timing?.offsetX ?? 0;
|
|
2164
|
+
const offsetY = options.timing?.offsetY ?? 0;
|
|
2165
|
+
const scaleX = options.timing?.scaleX ?? 1;
|
|
2166
|
+
const scaleY = options.timing?.scaleY ?? 1;
|
|
2167
|
+
const rotation = options.timing?.rotation ?? 0;
|
|
2168
|
+
element.style.width = `${options.displaySize.width}px`;
|
|
2169
|
+
element.style.height = `${options.displaySize.height}px`;
|
|
2170
|
+
setFrameElementBackground(frameElement, options);
|
|
2171
|
+
frameElement.style.transform =
|
|
2172
|
+
`translate(${offsetX}px, ${offsetY}px) scale(${scaleX}, ${scaleY}) rotate(${rotation}deg)`;
|
|
2173
|
+
frameElement.style.transformOrigin = "center";
|
|
2174
|
+
}
|
|
2175
|
+
export function setFrameElementBackground(frameElement, options) {
|
|
2176
|
+
const column = options.frame % options.frameGrid.columns;
|
|
2177
|
+
const row = Math.floor(options.frame / options.frameGrid.columns);
|
|
2178
|
+
frameElement.style.backgroundImage = `url("${cssUrl(options.src)}")`;
|
|
2179
|
+
frameElement.style.backgroundSize =
|
|
2180
|
+
`${options.frameGrid.columns * options.displaySize.width}px ${options.frameGrid.rows * options.displaySize.height}px`;
|
|
2181
|
+
frameElement.style.backgroundPosition =
|
|
2182
|
+
`${-(column * options.displaySize.width)}px ${-(row * options.displaySize.height)}px`;
|
|
2183
|
+
}
|
|
2184
|
+
export function ensureFramePreviewElement(element) {
|
|
2185
|
+
const existing = element.querySelector(":scope > .ai-game-assets-designer__frame-image");
|
|
2186
|
+
if (existing)
|
|
2187
|
+
return existing;
|
|
2188
|
+
const frameElement = document.createElement("span");
|
|
2189
|
+
frameElement.className = "ai-game-assets-designer__frame-image";
|
|
2190
|
+
element.append(frameElement);
|
|
2191
|
+
return frameElement;
|
|
2192
|
+
}
|
|
2193
|
+
export function generationOverridesFromInputs(elements, asset, format) {
|
|
2194
|
+
const dimensions = {
|
|
2195
|
+
width: positiveIntegerInput(elements.widthInput, asset.frameGrid?.frameWidth ?? asset.dimensions?.width ?? 1),
|
|
2196
|
+
height: positiveIntegerInput(elements.heightInput, asset.frameGrid?.frameHeight ?? asset.dimensions?.height ?? 1)
|
|
2197
|
+
};
|
|
2198
|
+
if (!asset.frameGrid) {
|
|
2199
|
+
return {
|
|
2200
|
+
dimensions,
|
|
2201
|
+
settings: { format }
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
return {
|
|
2205
|
+
dimensions,
|
|
2206
|
+
settings: { format },
|
|
2207
|
+
frameCount: positiveIntegerInput(elements.frameCountInput, asset.frameGrid.frameCount ?? asset.frameGrid.columns * asset.frameGrid.rows)
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
export function audioGenerationOverridesFromInputs(elements, asset) {
|
|
2211
|
+
if (!isAudioAsset(asset))
|
|
2212
|
+
return undefined;
|
|
2213
|
+
return {
|
|
2214
|
+
...asset.audioSettings,
|
|
2215
|
+
format: normalizeAudioFormat(elements.audioFormatSelect.value),
|
|
2216
|
+
durationSeconds: positiveNumberInput(elements.audioDurationInput, asset.audioSettings?.durationSeconds ?? (asset.kind === "music" ? 30 : 2)),
|
|
2217
|
+
loop: elements.audioLoopInput.checked
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
export function voiceGenerationOverridesFromInputs(elements, asset) {
|
|
2221
|
+
if (!isVoiceAsset(asset))
|
|
2222
|
+
return asset.voiceSettings;
|
|
2223
|
+
const text = elements.voiceTextInput.value.trim();
|
|
2224
|
+
if (asset.kind === "voice") {
|
|
2225
|
+
return {
|
|
2226
|
+
...asset.voiceSettings,
|
|
2227
|
+
previewText: text || asset.voiceSettings?.previewText
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
return {
|
|
2231
|
+
...asset.voiceSettings,
|
|
2232
|
+
text: text || asset.voiceSettings?.text,
|
|
2233
|
+
direction: elements.promptInput.value.trim() || asset.voiceSettings?.direction
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
export function canEditGenerationFormat(manifest, selectedAssetId, targetAssetId) {
|
|
2237
|
+
const selectedAsset = manifest.assets[selectedAssetId];
|
|
2238
|
+
return selectedAsset?.kind === "collection" || selectedAssetId === targetAssetId;
|
|
2239
|
+
}
|
|
2240
|
+
export function effectiveGenerationFormat(manifest, drafts, selectedAssetId, targetAssetId) {
|
|
2241
|
+
if (canEditGenerationFormat(manifest, selectedAssetId, targetAssetId)) {
|
|
2242
|
+
return drafts.get(targetAssetId) ?? selectedFormatFromDesignerOrAsset(manifest.assets[targetAssetId]);
|
|
2243
|
+
}
|
|
2244
|
+
return drafts.get(selectedAssetId) ?? selectedFormatFromDesignerOrAsset(manifest.assets[selectedAssetId]);
|
|
2245
|
+
}
|
|
2246
|
+
export function selectedFormatFromDesignerOrAsset(asset) {
|
|
2247
|
+
return normalizeAssetFormat(asset?.settings?.format);
|
|
2248
|
+
}
|
|
2249
|
+
export function normalizeAssetFormat(format) {
|
|
2250
|
+
if (format === "jpg" || format === "webp" || format === "svg")
|
|
2251
|
+
return format;
|
|
2252
|
+
return "png";
|
|
2253
|
+
}
|
|
2254
|
+
export function normalizeAudioFormat(format) {
|
|
2255
|
+
if (format === "wav" || format === "ogg" || format === "opus" || format === "pcm") {
|
|
2256
|
+
return format;
|
|
2257
|
+
}
|
|
2258
|
+
return "mp3";
|
|
2259
|
+
}
|
|
2260
|
+
export function isAudioAsset(asset) {
|
|
2261
|
+
return asset?.kind === "sound" ||
|
|
2262
|
+
asset?.kind === "music" ||
|
|
2263
|
+
asset?.kind === "voice" ||
|
|
2264
|
+
asset?.kind === "voice-line";
|
|
2265
|
+
}
|
|
2266
|
+
export function isVoiceAsset(asset) {
|
|
2267
|
+
return asset?.kind === "voice" || asset?.kind === "voice-line";
|
|
2268
|
+
}
|
|
2269
|
+
export function assetWithGeneratedGeometry(asset, option) {
|
|
2270
|
+
return {
|
|
2271
|
+
...asset,
|
|
2272
|
+
dimensions: option.dimensions ?? asset.dimensions,
|
|
2273
|
+
frameGrid: option.frameGrid ?? asset.frameGrid,
|
|
2274
|
+
animations: option.animations ?? asset.animations,
|
|
2275
|
+
audioPlayback: option.audioPlayback ?? asset.audioPlayback,
|
|
2276
|
+
voiceSettings: option.voiceSettings ?? asset.voiceSettings
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
export function positiveIntegerInput(input, fallback) {
|
|
2280
|
+
const value = Number(input.value);
|
|
2281
|
+
if (!Number.isFinite(value))
|
|
2282
|
+
return fallback;
|
|
2283
|
+
return Math.max(1, Math.floor(value));
|
|
2284
|
+
}
|
|
2285
|
+
export function positiveNumberInput(input, fallback) {
|
|
2286
|
+
const value = Number(input.value);
|
|
2287
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
2288
|
+
return fallback;
|
|
2289
|
+
return value;
|
|
2290
|
+
}
|
|
2291
|
+
export function integerInput(input, fallback) {
|
|
2292
|
+
const value = Number(input.value);
|
|
2293
|
+
if (!Number.isFinite(value))
|
|
2294
|
+
return fallback;
|
|
2295
|
+
return Math.trunc(value);
|
|
2296
|
+
}
|
|
2297
|
+
export function numberInput(input, fallback) {
|
|
2298
|
+
const value = Number(input.value);
|
|
2299
|
+
if (!Number.isFinite(value))
|
|
2300
|
+
return fallback;
|
|
2301
|
+
return value;
|
|
2302
|
+
}
|
|
2303
|
+
export function rangeInput(min, max, step) {
|
|
2304
|
+
const input = document.createElement("input");
|
|
2305
|
+
input.type = "range";
|
|
2306
|
+
input.min = String(min);
|
|
2307
|
+
input.max = String(max);
|
|
2308
|
+
input.step = String(step);
|
|
2309
|
+
return input;
|
|
2310
|
+
}
|
|
2311
|
+
export function inlineCheckboxField(label, checkbox) {
|
|
2312
|
+
const wrapper = document.createElement("label");
|
|
2313
|
+
wrapper.className = "ai-game-assets-designer__inline-checkbox";
|
|
2314
|
+
const text = document.createElement("span");
|
|
2315
|
+
text.textContent = label;
|
|
2316
|
+
wrapper.append(checkbox, text);
|
|
2317
|
+
return wrapper;
|
|
2318
|
+
}
|
|
2319
|
+
export function positiveIntegerValue(value, fallback) {
|
|
2320
|
+
if (!Number.isFinite(value))
|
|
2321
|
+
return fallback;
|
|
2322
|
+
return Math.max(1, Math.floor(value));
|
|
2323
|
+
}
|
|
2324
|
+
export function integerValue(value, fallback) {
|
|
2325
|
+
if (!Number.isFinite(value))
|
|
2326
|
+
return fallback;
|
|
2327
|
+
return Math.trunc(value);
|
|
2328
|
+
}
|
|
2329
|
+
export function numberValue(value, fallback) {
|
|
2330
|
+
if (!Number.isFinite(value))
|
|
2331
|
+
return fallback;
|
|
2332
|
+
return value;
|
|
2333
|
+
}
|
|
2334
|
+
export function resolvePreviewDisplaySize(options, assetId, asset) {
|
|
2335
|
+
const configured = typeof options.previewDisplaySize === "function"
|
|
2336
|
+
? options.previewDisplaySize(assetId, asset)
|
|
2337
|
+
: options.previewDisplaySize?.[assetId];
|
|
2338
|
+
if (configured) {
|
|
2339
|
+
return configured;
|
|
2340
|
+
}
|
|
2341
|
+
return {
|
|
2342
|
+
width: asset.frameGrid?.frameWidth ?? asset.dimensions?.width ?? 128,
|
|
2343
|
+
height: asset.frameGrid?.frameHeight ?? asset.dimensions?.height ?? 128
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
export function cssUrl(value) {
|
|
2347
|
+
return value.replace(/["\\]/g, "\\$&");
|
|
2348
|
+
}
|
|
2349
|
+
export function bindKeyboardCapture(root, scene) {
|
|
2350
|
+
const stopKeyboardEvent = (event) => {
|
|
2351
|
+
const target = event.target;
|
|
2352
|
+
if (target instanceof HTMLInputElement ||
|
|
2353
|
+
target instanceof HTMLTextAreaElement ||
|
|
2354
|
+
target instanceof HTMLSelectElement) {
|
|
2355
|
+
event.stopPropagation();
|
|
2356
|
+
}
|
|
2357
|
+
};
|
|
2358
|
+
const setKeyboardEnabled = (enabled) => {
|
|
2359
|
+
if (scene.input?.keyboard) {
|
|
2360
|
+
scene.input.keyboard.enabled = enabled;
|
|
2361
|
+
}
|
|
2362
|
+
root.dataset.keyboardCaptured = String(!enabled);
|
|
2363
|
+
};
|
|
2364
|
+
root.addEventListener("keydown", stopKeyboardEvent, true);
|
|
2365
|
+
root.addEventListener("keyup", stopKeyboardEvent, true);
|
|
2366
|
+
root.addEventListener("focusin", (event) => {
|
|
2367
|
+
if (event.target instanceof HTMLElement && isEditableElement(event.target)) {
|
|
2368
|
+
setKeyboardEnabled(false);
|
|
2369
|
+
}
|
|
2370
|
+
});
|
|
2371
|
+
root.addEventListener("focusout", () => setKeyboardEnabled(true));
|
|
2372
|
+
}
|
|
2373
|
+
export function isEditableElement(element) {
|
|
2374
|
+
return (element instanceof HTMLInputElement ||
|
|
2375
|
+
element instanceof HTMLTextAreaElement ||
|
|
2376
|
+
element instanceof HTMLSelectElement ||
|
|
2377
|
+
element.isContentEditable);
|
|
2378
|
+
}
|
|
2379
|
+
export function labelWrap(labelText, control) {
|
|
2380
|
+
const label = document.createElement("label");
|
|
2381
|
+
label.className = "ai-game-assets-designer__field";
|
|
2382
|
+
const span = document.createElement("span");
|
|
2383
|
+
span.textContent = labelText;
|
|
2384
|
+
label.append(span, control);
|
|
2385
|
+
return label;
|
|
2386
|
+
}
|
|
2387
|
+
export function numericInput() {
|
|
2388
|
+
const input = document.createElement("input");
|
|
2389
|
+
input.type = "number";
|
|
2390
|
+
input.min = "1";
|
|
2391
|
+
input.step = "1";
|
|
2392
|
+
input.inputMode = "numeric";
|
|
2393
|
+
return input;
|
|
2394
|
+
}
|
|
2395
|
+
export function signedNumberInput() {
|
|
2396
|
+
const input = document.createElement("input");
|
|
2397
|
+
input.type = "number";
|
|
2398
|
+
input.step = "1";
|
|
2399
|
+
input.inputMode = "numeric";
|
|
2400
|
+
return input;
|
|
2401
|
+
}
|
|
2402
|
+
export function decimalInput() {
|
|
2403
|
+
const input = document.createElement("input");
|
|
2404
|
+
input.type = "number";
|
|
2405
|
+
input.step = "any";
|
|
2406
|
+
input.inputMode = "decimal";
|
|
2407
|
+
return input;
|
|
2408
|
+
}
|
|
2409
|
+
export async function imageSourceToDataUrl(src) {
|
|
2410
|
+
if (src.startsWith("data:"))
|
|
2411
|
+
return src;
|
|
2412
|
+
const response = await fetch(src);
|
|
2413
|
+
if (!response.ok) {
|
|
2414
|
+
throw new Error(`Could not load current animation image (${response.status}).`);
|
|
2415
|
+
}
|
|
2416
|
+
const responseBlob = await response.blob();
|
|
2417
|
+
const blob = responseBlob.type === "application/octet-stream"
|
|
2418
|
+
? responseBlob.slice(0, responseBlob.size, mimeTypeFromFileName(src))
|
|
2419
|
+
: responseBlob;
|
|
2420
|
+
return new Promise((resolve, reject) => {
|
|
2421
|
+
const reader = new FileReader();
|
|
2422
|
+
reader.addEventListener("load", () => {
|
|
2423
|
+
if (typeof reader.result === "string") {
|
|
2424
|
+
resolve(reader.result);
|
|
2425
|
+
}
|
|
2426
|
+
else {
|
|
2427
|
+
reject(new Error("Could not convert current animation image to a data URL."));
|
|
2428
|
+
}
|
|
2429
|
+
});
|
|
2430
|
+
reader.addEventListener("error", () => {
|
|
2431
|
+
reject(reader.error ?? new Error("Could not read current animation image."));
|
|
2432
|
+
});
|
|
2433
|
+
reader.readAsDataURL(blob);
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
export function styleGuideDraftFromManifest(manifest) {
|
|
2437
|
+
return {
|
|
2438
|
+
prompt: manifest.styleGuide?.prompt ?? "",
|
|
2439
|
+
images: (manifest.styleGuide?.images ?? []).map((image) => ({
|
|
2440
|
+
name: image.name,
|
|
2441
|
+
src: image.file
|
|
2442
|
+
}))
|
|
2443
|
+
};
|
|
2444
|
+
}
|
|
2445
|
+
export function hasStyleGuide(styleGuide) {
|
|
2446
|
+
return Boolean(styleGuide.prompt.trim() || styleGuide.images.length);
|
|
2447
|
+
}
|
|
2448
|
+
export async function styleGuideRequest(styleGuide) {
|
|
2449
|
+
return {
|
|
2450
|
+
prompt: styleGuide.prompt.trim() || undefined,
|
|
2451
|
+
images: await Promise.all(styleGuide.images.map(async (image) => ({
|
|
2452
|
+
name: image.name,
|
|
2453
|
+
dataUrl: await imageSourceToDataUrl(image.src)
|
|
2454
|
+
})))
|
|
2455
|
+
};
|
|
2456
|
+
}
|
|
2457
|
+
export function fileToDataUrl(file) {
|
|
2458
|
+
return new Promise((resolve, reject) => {
|
|
2459
|
+
const reader = new FileReader();
|
|
2460
|
+
reader.addEventListener("load", () => {
|
|
2461
|
+
if (typeof reader.result === "string") {
|
|
2462
|
+
resolve(reader.result);
|
|
2463
|
+
}
|
|
2464
|
+
else {
|
|
2465
|
+
reject(new Error("Could not read uploaded file."));
|
|
2466
|
+
}
|
|
2467
|
+
});
|
|
2468
|
+
reader.addEventListener("error", () => {
|
|
2469
|
+
reject(reader.error ?? new Error("Could not read uploaded file."));
|
|
2470
|
+
});
|
|
2471
|
+
reader.readAsDataURL(file);
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2474
|
+
export function mimeTypeFromDataUrl(dataUrl) {
|
|
2475
|
+
return /^data:([^;,]+)/.exec(dataUrl)?.[1] ?? "image/png";
|
|
2476
|
+
}
|
|
2477
|
+
export function mimeTypeFromFileName(fileName) {
|
|
2478
|
+
if (/\.png(?:$|[?#])/i.test(fileName))
|
|
2479
|
+
return "image/png";
|
|
2480
|
+
if (/\.webp(?:$|[?#])/i.test(fileName))
|
|
2481
|
+
return "image/webp";
|
|
2482
|
+
if (/\.jpe?g(?:$|[?#])/i.test(fileName))
|
|
2483
|
+
return "image/jpeg";
|
|
2484
|
+
if (/\.svg(?:$|[?#])/i.test(fileName))
|
|
2485
|
+
return "image/svg+xml";
|
|
2486
|
+
return "image/png";
|
|
2487
|
+
}
|
|
2488
|
+
export function readableAssetName(assetId) {
|
|
2489
|
+
const displayId = assetId.startsWith("audio.sfx.")
|
|
2490
|
+
? assetId.slice("audio.sfx.".length)
|
|
2491
|
+
: assetId;
|
|
2492
|
+
return displayId
|
|
2493
|
+
.split(/[._-]/g)
|
|
2494
|
+
.map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
|
|
2495
|
+
.join(" ");
|
|
2496
|
+
}
|
|
2497
|
+
export function errorMessage(error) {
|
|
2498
|
+
return error instanceof Error ? error.message : String(error);
|
|
2499
|
+
}
|
|
2500
|
+
export function isAbortError(error) {
|
|
2501
|
+
return error instanceof DOMException
|
|
2502
|
+
? error.name === "AbortError"
|
|
2503
|
+
: error instanceof Error && error.name === "AbortError";
|
|
2504
|
+
}
|
|
2505
|
+
const statusAnimationTimers = new WeakMap();
|
|
2506
|
+
export function setStatus(elements, message, kind) {
|
|
2507
|
+
if (elements.status.dataset.statusLock === "generation" &&
|
|
2508
|
+
(kind !== "busy" || message !== "Generating options...")) {
|
|
2509
|
+
return;
|
|
2510
|
+
}
|
|
2511
|
+
stopStatusAnimation(elements.status);
|
|
2512
|
+
elements.status.dataset.kind = kind;
|
|
2513
|
+
if (kind === "busy" && message === "Generating options...") {
|
|
2514
|
+
startGeneratingStatusAnimation(elements.status, message);
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
elements.status.textContent = message;
|
|
2518
|
+
}
|
|
2519
|
+
export function stopStatusAnimation(status) {
|
|
2520
|
+
const timer = statusAnimationTimers.get(status);
|
|
2521
|
+
if (timer !== undefined) {
|
|
2522
|
+
window.clearInterval(timer);
|
|
2523
|
+
statusAnimationTimers.delete(status);
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
export function startGeneratingStatusAnimation(status, message) {
|
|
2527
|
+
let offset = 0;
|
|
2528
|
+
const highlightLength = 3;
|
|
2529
|
+
const maxOffset = Math.max(1, message.length - highlightLength + 1);
|
|
2530
|
+
const render = () => {
|
|
2531
|
+
const before = message.slice(0, offset);
|
|
2532
|
+
const highlighted = message.slice(offset, offset + highlightLength);
|
|
2533
|
+
const after = message.slice(offset + highlightLength);
|
|
2534
|
+
const highlight = document.createElement("span");
|
|
2535
|
+
highlight.className = "ai-game-assets-designer__status-highlight";
|
|
2536
|
+
highlight.textContent = highlighted;
|
|
2537
|
+
status.replaceChildren(document.createTextNode(before), highlight, document.createTextNode(after));
|
|
2538
|
+
offset = (offset + 1) % maxOffset;
|
|
2539
|
+
};
|
|
2540
|
+
render();
|
|
2541
|
+
statusAnimationTimers.set(status, window.setInterval(render, 140));
|
|
2542
|
+
}
|
|
2543
|
+
export function ensureDesignerStyles() {
|
|
2544
|
+
const styleId = "ai-game-assets-designer-styles";
|
|
2545
|
+
if (document.getElementById(styleId)) {
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
const style = document.createElement("style");
|
|
2549
|
+
style.id = styleId;
|
|
2550
|
+
style.textContent = `
|
|
2551
|
+
.ai-game-assets-designer {
|
|
2552
|
+
position: fixed;
|
|
2553
|
+
top: 14px;
|
|
2554
|
+
right: 14px;
|
|
2555
|
+
z-index: 2147483647;
|
|
2556
|
+
color: #f5f7fb;
|
|
2557
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2558
|
+
}
|
|
2559
|
+
.ai-game-assets-designer * { box-sizing: border-box; }
|
|
2560
|
+
.ai-game-assets-designer [hidden] { display: none !important; }
|
|
2561
|
+
.ai-game-assets-designer__toggle {
|
|
2562
|
+
min-width: 74px;
|
|
2563
|
+
height: 42px;
|
|
2564
|
+
padding: 0 16px;
|
|
2565
|
+
border: 1px solid #63708a;
|
|
2566
|
+
border-radius: 999px;
|
|
2567
|
+
background: #202838;
|
|
2568
|
+
color: #fff;
|
|
2569
|
+
font-weight: 700;
|
|
2570
|
+
cursor: pointer;
|
|
2571
|
+
box-shadow: 0 10px 26px rgba(0, 0, 0, 0.35);
|
|
2572
|
+
transition: background 140ms ease, border-color 140ms ease, box-shadow 140ms ease, transform 140ms ease;
|
|
2573
|
+
}
|
|
2574
|
+
.ai-game-assets-designer__toggle:hover,
|
|
2575
|
+
.ai-game-assets-designer__toggle:focus-visible {
|
|
2576
|
+
border-color: #8bb8ff;
|
|
2577
|
+
background: #253149;
|
|
2578
|
+
box-shadow:
|
|
2579
|
+
0 0 0 3px rgba(74, 144, 255, 0.24),
|
|
2580
|
+
0 0 22px rgba(74, 144, 255, 0.42),
|
|
2581
|
+
0 12px 30px rgba(0, 0, 0, 0.42);
|
|
2582
|
+
transform: translateY(-1px);
|
|
2583
|
+
}
|
|
2584
|
+
.ai-game-assets-designer__panel {
|
|
2585
|
+
display: none;
|
|
2586
|
+
width: min(340px, calc(100vw - 28px));
|
|
2587
|
+
max-height: calc(100vh - 80px);
|
|
2588
|
+
overflow-y: auto;
|
|
2589
|
+
margin-top: 10px;
|
|
2590
|
+
padding: 14px;
|
|
2591
|
+
border: 1px solid #303949;
|
|
2592
|
+
border-radius: 8px;
|
|
2593
|
+
background: rgba(20, 24, 32, 0.97);
|
|
2594
|
+
box-shadow: 0 18px 60px rgba(0, 0, 0, 0.45);
|
|
2595
|
+
}
|
|
2596
|
+
.ai-game-assets-designer[data-open="true"] .ai-game-assets-designer__panel { display: block; }
|
|
2597
|
+
.ai-game-assets-designer__header {
|
|
2598
|
+
display: flex;
|
|
2599
|
+
align-items: center;
|
|
2600
|
+
justify-content: space-between;
|
|
2601
|
+
gap: 8px;
|
|
2602
|
+
margin-bottom: 12px;
|
|
2603
|
+
}
|
|
2604
|
+
.ai-game-assets-designer__title {
|
|
2605
|
+
font-weight: 700;
|
|
2606
|
+
font-size: 15px;
|
|
2607
|
+
}
|
|
2608
|
+
.ai-game-assets-designer__style-button {
|
|
2609
|
+
border: 1px solid #58657a;
|
|
2610
|
+
border-radius: 6px;
|
|
2611
|
+
background: #273142;
|
|
2612
|
+
color: #fff;
|
|
2613
|
+
padding: 6px 8px;
|
|
2614
|
+
font: inherit;
|
|
2615
|
+
font-size: 12px;
|
|
2616
|
+
cursor: pointer;
|
|
2617
|
+
}
|
|
2618
|
+
.ai-game-assets-designer__field {
|
|
2619
|
+
display: grid;
|
|
2620
|
+
gap: 7px;
|
|
2621
|
+
margin-bottom: 12px;
|
|
2622
|
+
color: #b9c1cf;
|
|
2623
|
+
font-size: 13px;
|
|
2624
|
+
}
|
|
2625
|
+
.ai-game-assets-designer__field select,
|
|
2626
|
+
.ai-game-assets-designer__field input,
|
|
2627
|
+
.ai-game-assets-designer__field textarea {
|
|
2628
|
+
width: 100%;
|
|
2629
|
+
border: 1px solid #3a4352;
|
|
2630
|
+
border-radius: 6px;
|
|
2631
|
+
background: #101319;
|
|
2632
|
+
color: #f5f7fb;
|
|
2633
|
+
padding: 9px 10px;
|
|
2634
|
+
font: inherit;
|
|
2635
|
+
}
|
|
2636
|
+
.ai-game-assets-designer__field textarea { resize: vertical; }
|
|
2637
|
+
.ai-game-assets-designer__asset-browser {
|
|
2638
|
+
display: grid;
|
|
2639
|
+
gap: 8px;
|
|
2640
|
+
padding: 8px;
|
|
2641
|
+
border: 1px solid #303949;
|
|
2642
|
+
border-radius: 8px;
|
|
2643
|
+
background: rgba(11, 15, 22, 0.58);
|
|
2644
|
+
}
|
|
2645
|
+
.ai-game-assets-designer__asset-breadcrumbs {
|
|
2646
|
+
display: flex;
|
|
2647
|
+
flex-wrap: wrap;
|
|
2648
|
+
align-items: center;
|
|
2649
|
+
gap: 5px;
|
|
2650
|
+
color: #8f9cb5;
|
|
2651
|
+
font-size: 12px;
|
|
2652
|
+
}
|
|
2653
|
+
.ai-game-assets-designer__asset-breadcrumb,
|
|
2654
|
+
.ai-game-assets-designer__asset-folder,
|
|
2655
|
+
.ai-game-assets-designer__asset-item {
|
|
2656
|
+
border: 1px solid #374155;
|
|
2657
|
+
border-radius: 6px;
|
|
2658
|
+
background: #151c29;
|
|
2659
|
+
color: #eef4ff;
|
|
2660
|
+
cursor: pointer;
|
|
2661
|
+
}
|
|
2662
|
+
.ai-game-assets-designer__asset-breadcrumb {
|
|
2663
|
+
padding: 3px 7px;
|
|
2664
|
+
font-size: 12px;
|
|
2665
|
+
}
|
|
2666
|
+
.ai-game-assets-designer__asset-breadcrumb:hover,
|
|
2667
|
+
.ai-game-assets-designer__asset-folder:hover,
|
|
2668
|
+
.ai-game-assets-designer__asset-item:hover {
|
|
2669
|
+
border-color: #6ea6ff;
|
|
2670
|
+
background: #202b40;
|
|
2671
|
+
}
|
|
2672
|
+
.ai-game-assets-designer__asset-breadcrumb-separator { opacity: 0.65; }
|
|
2673
|
+
.ai-game-assets-designer__asset-list {
|
|
2674
|
+
display: grid;
|
|
2675
|
+
gap: 6px;
|
|
2676
|
+
max-height: 145px;
|
|
2677
|
+
overflow-y: auto;
|
|
2678
|
+
}
|
|
2679
|
+
.ai-game-assets-designer__asset-folder,
|
|
2680
|
+
.ai-game-assets-designer__asset-item {
|
|
2681
|
+
width: 100%;
|
|
2682
|
+
min-height: 30px;
|
|
2683
|
+
padding: 6px 8px;
|
|
2684
|
+
text-align: left;
|
|
2685
|
+
}
|
|
2686
|
+
.ai-game-assets-designer__asset-folder::before {
|
|
2687
|
+
content: ">";
|
|
2688
|
+
display: inline-block;
|
|
2689
|
+
margin-right: 7px;
|
|
2690
|
+
color: #92b8ff;
|
|
2691
|
+
}
|
|
2692
|
+
.ai-game-assets-designer__asset-item.is-selected {
|
|
2693
|
+
border-color: #8bb8ff;
|
|
2694
|
+
background: #263450;
|
|
2695
|
+
box-shadow: inset 3px 0 0 #8bb8ff;
|
|
2696
|
+
}
|
|
2697
|
+
.ai-game-assets-designer__dimensions {
|
|
2698
|
+
display: grid;
|
|
2699
|
+
grid-template-columns: 1fr 1fr;
|
|
2700
|
+
gap: 8px;
|
|
2701
|
+
}
|
|
2702
|
+
.ai-game-assets-designer__dimensions .ai-game-assets-designer__field {
|
|
2703
|
+
margin-bottom: 12px;
|
|
2704
|
+
}
|
|
2705
|
+
.ai-game-assets-designer__current {
|
|
2706
|
+
min-height: 96px;
|
|
2707
|
+
display: grid;
|
|
2708
|
+
place-items: center;
|
|
2709
|
+
padding: 8px;
|
|
2710
|
+
border: 1px solid #384251;
|
|
2711
|
+
border-radius: 6px;
|
|
2712
|
+
background: #0f1218;
|
|
2713
|
+
overflow: hidden;
|
|
2714
|
+
cursor: pointer;
|
|
2715
|
+
}
|
|
2716
|
+
.ai-game-assets-designer__current.is-selected {
|
|
2717
|
+
border-color: #6ed3ff;
|
|
2718
|
+
}
|
|
2719
|
+
.ai-game-assets-designer__current:focus-visible {
|
|
2720
|
+
outline: 2px solid #93c5fd;
|
|
2721
|
+
outline-offset: 2px;
|
|
2722
|
+
}
|
|
2723
|
+
.ai-game-assets-designer__current-image {
|
|
2724
|
+
max-width: 100%;
|
|
2725
|
+
max-height: 112px;
|
|
2726
|
+
object-fit: contain;
|
|
2727
|
+
image-rendering: pixelated;
|
|
2728
|
+
}
|
|
2729
|
+
.ai-game-assets-designer__current-audio {
|
|
2730
|
+
width: 100%;
|
|
2731
|
+
}
|
|
2732
|
+
.ai-game-assets-designer__audio-player {
|
|
2733
|
+
width: 100%;
|
|
2734
|
+
display: grid;
|
|
2735
|
+
gap: 8px;
|
|
2736
|
+
}
|
|
2737
|
+
.ai-game-assets-designer__audio-player audio {
|
|
2738
|
+
display: none;
|
|
2739
|
+
}
|
|
2740
|
+
.ai-game-assets-designer__audio-controls {
|
|
2741
|
+
display: grid;
|
|
2742
|
+
grid-template-columns: auto 1fr;
|
|
2743
|
+
align-items: center;
|
|
2744
|
+
gap: 8px;
|
|
2745
|
+
}
|
|
2746
|
+
.ai-game-assets-designer__audio-play {
|
|
2747
|
+
min-width: 56px;
|
|
2748
|
+
border: 1px solid #58657a;
|
|
2749
|
+
border-radius: 6px;
|
|
2750
|
+
background: #273142;
|
|
2751
|
+
color: #fff;
|
|
2752
|
+
padding: 6px 8px;
|
|
2753
|
+
font: inherit;
|
|
2754
|
+
font-size: 12px;
|
|
2755
|
+
cursor: pointer;
|
|
2756
|
+
}
|
|
2757
|
+
.ai-game-assets-designer__audio-time {
|
|
2758
|
+
color: #cbd5e1;
|
|
2759
|
+
font-size: 12px;
|
|
2760
|
+
font-variant-numeric: tabular-nums;
|
|
2761
|
+
text-align: right;
|
|
2762
|
+
}
|
|
2763
|
+
.ai-game-assets-designer__audio-trim {
|
|
2764
|
+
color: #fbbf24;
|
|
2765
|
+
font-size: 11px;
|
|
2766
|
+
font-variant-numeric: tabular-nums;
|
|
2767
|
+
text-align: right;
|
|
2768
|
+
}
|
|
2769
|
+
.ai-game-assets-designer__audio-waveform {
|
|
2770
|
+
width: 100%;
|
|
2771
|
+
height: 58px;
|
|
2772
|
+
border: 1px solid #2f3a49;
|
|
2773
|
+
border-radius: 6px;
|
|
2774
|
+
background: #111827;
|
|
2775
|
+
cursor: pointer;
|
|
2776
|
+
}
|
|
2777
|
+
.ai-game-assets-designer__animation-stage,
|
|
2778
|
+
.ai-game-assets-designer__option-animation {
|
|
2779
|
+
position: relative;
|
|
2780
|
+
overflow: hidden;
|
|
2781
|
+
background: #0f1218;
|
|
2782
|
+
}
|
|
2783
|
+
.ai-game-assets-designer__animate-button {
|
|
2784
|
+
border: 1px solid #58657a;
|
|
2785
|
+
border-radius: 6px;
|
|
2786
|
+
background: #273142;
|
|
2787
|
+
color: #fff;
|
|
2788
|
+
padding: 6px 8px;
|
|
2789
|
+
font: inherit;
|
|
2790
|
+
font-size: 12px;
|
|
2791
|
+
cursor: pointer;
|
|
2792
|
+
}
|
|
2793
|
+
.ai-game-assets-designer__current .ai-game-assets-designer__animate-button {
|
|
2794
|
+
width: 100%;
|
|
2795
|
+
}
|
|
2796
|
+
.ai-game-assets-designer__actions {
|
|
2797
|
+
display: grid;
|
|
2798
|
+
grid-template-columns: 1fr 1fr;
|
|
2799
|
+
gap: 8px;
|
|
2800
|
+
}
|
|
2801
|
+
.ai-game-assets-designer__actions button {
|
|
2802
|
+
border: 1px solid #58657a;
|
|
2803
|
+
border-radius: 6px;
|
|
2804
|
+
background: #273142;
|
|
2805
|
+
color: #fff;
|
|
2806
|
+
padding: 9px 11px;
|
|
2807
|
+
font: inherit;
|
|
2808
|
+
cursor: pointer;
|
|
2809
|
+
}
|
|
2810
|
+
.ai-game-assets-designer__actions button:last-child { grid-column: 1 / -1; }
|
|
2811
|
+
.ai-game-assets-designer__actions button:disabled {
|
|
2812
|
+
cursor: not-allowed;
|
|
2813
|
+
opacity: 0.45;
|
|
2814
|
+
}
|
|
2815
|
+
.ai-game-assets-designer__meta,
|
|
2816
|
+
.ai-game-assets-designer__status {
|
|
2817
|
+
min-height: 22px;
|
|
2818
|
+
color: #b9c1cf;
|
|
2819
|
+
font-size: 13px;
|
|
2820
|
+
margin-top: 11px;
|
|
2821
|
+
}
|
|
2822
|
+
.ai-game-assets-designer__status {
|
|
2823
|
+
line-height: 1.35;
|
|
2824
|
+
overflow-wrap: anywhere;
|
|
2825
|
+
white-space: pre-wrap;
|
|
2826
|
+
}
|
|
2827
|
+
.ai-game-assets-designer__status[data-kind="error"] {
|
|
2828
|
+
min-height: 0;
|
|
2829
|
+
padding: 10px 11px;
|
|
2830
|
+
border: 1px solid #f87171;
|
|
2831
|
+
border-radius: 6px;
|
|
2832
|
+
background: rgba(127, 29, 29, 0.34);
|
|
2833
|
+
color: #fecaca;
|
|
2834
|
+
}
|
|
2835
|
+
.ai-game-assets-designer__status[data-kind="success"] {
|
|
2836
|
+
color: #bbf7d0;
|
|
2837
|
+
}
|
|
2838
|
+
.ai-game-assets-designer__status[data-kind="busy"] {
|
|
2839
|
+
color: #bfdbfe;
|
|
2840
|
+
}
|
|
2841
|
+
.ai-game-assets-designer__status-highlight {
|
|
2842
|
+
color: #ffffff;
|
|
2843
|
+
text-shadow: 0 0 10px rgba(147, 197, 253, 0.9);
|
|
2844
|
+
}
|
|
2845
|
+
.ai-game-assets-designer__options {
|
|
2846
|
+
display: grid;
|
|
2847
|
+
grid-template-columns: repeat(auto-fit, minmax(82px, 1fr));
|
|
2848
|
+
gap: 8px;
|
|
2849
|
+
margin-top: 10px;
|
|
2850
|
+
}
|
|
2851
|
+
.ai-game-assets-designer__options.is-audio {
|
|
2852
|
+
grid-template-columns: 1fr;
|
|
2853
|
+
}
|
|
2854
|
+
.ai-game-assets-designer__option {
|
|
2855
|
+
border: 2px solid #384251;
|
|
2856
|
+
border-radius: 8px;
|
|
2857
|
+
background: #0f1218;
|
|
2858
|
+
min-height: 86px;
|
|
2859
|
+
display: grid;
|
|
2860
|
+
gap: 6px;
|
|
2861
|
+
place-items: center;
|
|
2862
|
+
padding: 6px;
|
|
2863
|
+
}
|
|
2864
|
+
.ai-game-assets-designer__option.is-selected { border-color: #6ed3ff; }
|
|
2865
|
+
.ai-game-assets-designer__options.is-audio .ai-game-assets-designer__option {
|
|
2866
|
+
grid-template-columns: 120px 1fr;
|
|
2867
|
+
align-items: center;
|
|
2868
|
+
min-height: 112px;
|
|
2869
|
+
}
|
|
2870
|
+
.ai-game-assets-designer__option-select {
|
|
2871
|
+
width: 100%;
|
|
2872
|
+
min-height: 74px;
|
|
2873
|
+
border: 0;
|
|
2874
|
+
background: transparent;
|
|
2875
|
+
display: grid;
|
|
2876
|
+
place-items: center;
|
|
2877
|
+
cursor: pointer;
|
|
2878
|
+
color: #dbeafe;
|
|
2879
|
+
font: inherit;
|
|
2880
|
+
}
|
|
2881
|
+
.ai-game-assets-designer__option-select--audio {
|
|
2882
|
+
min-height: 72px;
|
|
2883
|
+
border: 1px solid #344155;
|
|
2884
|
+
border-radius: 6px;
|
|
2885
|
+
background: #172033;
|
|
2886
|
+
padding: 8px;
|
|
2887
|
+
}
|
|
2888
|
+
.ai-game-assets-designer__option-audio {
|
|
2889
|
+
width: 100%;
|
|
2890
|
+
}
|
|
2891
|
+
.ai-game-assets-designer__option-select img {
|
|
2892
|
+
width: 68px;
|
|
2893
|
+
height: 68px;
|
|
2894
|
+
object-fit: contain;
|
|
2895
|
+
image-rendering: pixelated;
|
|
2896
|
+
}
|
|
2897
|
+
.ai-game-assets-designer__option .ai-game-assets-designer__animate-button {
|
|
2898
|
+
width: 100%;
|
|
2899
|
+
}
|
|
2900
|
+
.ai-game-assets-designer__modal {
|
|
2901
|
+
position: fixed;
|
|
2902
|
+
inset: 0;
|
|
2903
|
+
display: grid;
|
|
2904
|
+
place-items: center;
|
|
2905
|
+
padding: 18px;
|
|
2906
|
+
background: rgba(6, 8, 12, 0.62);
|
|
2907
|
+
}
|
|
2908
|
+
.ai-game-assets-designer__modal-card {
|
|
2909
|
+
width: min(520px, calc(100vw - 36px));
|
|
2910
|
+
max-height: calc(100vh - 36px);
|
|
2911
|
+
overflow: auto;
|
|
2912
|
+
border: 1px solid #384251;
|
|
2913
|
+
border-radius: 8px;
|
|
2914
|
+
background: #141820;
|
|
2915
|
+
box-shadow: 0 22px 70px rgba(0, 0, 0, 0.55);
|
|
2916
|
+
padding: 14px;
|
|
2917
|
+
}
|
|
2918
|
+
.ai-game-assets-designer__modal-title {
|
|
2919
|
+
margin-bottom: 12px;
|
|
2920
|
+
font-weight: 700;
|
|
2921
|
+
font-size: 15px;
|
|
2922
|
+
}
|
|
2923
|
+
.ai-game-assets-designer__modal-stage {
|
|
2924
|
+
margin: 0 auto 14px;
|
|
2925
|
+
position: relative;
|
|
2926
|
+
overflow: hidden;
|
|
2927
|
+
background: #0f1218;
|
|
2928
|
+
}
|
|
2929
|
+
.ai-game-assets-designer__frame-image {
|
|
2930
|
+
position: absolute;
|
|
2931
|
+
inset: 0;
|
|
2932
|
+
background-repeat: no-repeat;
|
|
2933
|
+
image-rendering: pixelated;
|
|
2934
|
+
}
|
|
2935
|
+
.ai-game-assets-designer__frame-strip {
|
|
2936
|
+
display: flex;
|
|
2937
|
+
gap: 8px;
|
|
2938
|
+
overflow-x: auto;
|
|
2939
|
+
padding: 8px 0 12px;
|
|
2940
|
+
}
|
|
2941
|
+
.ai-game-assets-designer__frame-thumb {
|
|
2942
|
+
flex: 0 0 auto;
|
|
2943
|
+
border: 2px solid #384251;
|
|
2944
|
+
border-radius: 6px;
|
|
2945
|
+
background-color: #0f1218;
|
|
2946
|
+
position: relative;
|
|
2947
|
+
overflow: hidden;
|
|
2948
|
+
cursor: pointer;
|
|
2949
|
+
}
|
|
2950
|
+
.ai-game-assets-designer__frame-thumb.is-selected {
|
|
2951
|
+
border-color: #6ed3ff;
|
|
2952
|
+
}
|
|
2953
|
+
.ai-game-assets-designer__frame-fields {
|
|
2954
|
+
display: grid;
|
|
2955
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
2956
|
+
gap: 8px;
|
|
2957
|
+
}
|
|
2958
|
+
.ai-game-assets-designer__frame-fields .ai-game-assets-designer__field {
|
|
2959
|
+
margin-bottom: 0;
|
|
2960
|
+
}
|
|
2961
|
+
.ai-game-assets-designer__audio-editor-stage {
|
|
2962
|
+
display: grid;
|
|
2963
|
+
gap: 6px;
|
|
2964
|
+
padding: 8px;
|
|
2965
|
+
border: 1px solid #384251;
|
|
2966
|
+
border-radius: 6px;
|
|
2967
|
+
background: #0f1218;
|
|
2968
|
+
}
|
|
2969
|
+
.ai-game-assets-designer__audio-editor-waveform {
|
|
2970
|
+
width: 100%;
|
|
2971
|
+
height: 132px;
|
|
2972
|
+
border: 1px solid #2f3a49;
|
|
2973
|
+
border-radius: 6px;
|
|
2974
|
+
background: #111827;
|
|
2975
|
+
cursor: ew-resize;
|
|
2976
|
+
}
|
|
2977
|
+
.ai-game-assets-designer__audio-editor-scrubber {
|
|
2978
|
+
width: 100%;
|
|
2979
|
+
height: 18px;
|
|
2980
|
+
margin: 0;
|
|
2981
|
+
cursor: pointer;
|
|
2982
|
+
}
|
|
2983
|
+
.ai-game-assets-designer__audio-editor-transport {
|
|
2984
|
+
display: grid;
|
|
2985
|
+
grid-template-columns: auto minmax(88px, auto) 1fr;
|
|
2986
|
+
align-items: center;
|
|
2987
|
+
gap: 8px;
|
|
2988
|
+
margin-top: 10px;
|
|
2989
|
+
}
|
|
2990
|
+
.ai-game-assets-designer__audio-editor-transport .ai-game-assets-designer__field {
|
|
2991
|
+
margin: 0;
|
|
2992
|
+
}
|
|
2993
|
+
.ai-game-assets-designer__inline-checkbox {
|
|
2994
|
+
display: flex;
|
|
2995
|
+
align-items: center;
|
|
2996
|
+
justify-content: flex-start;
|
|
2997
|
+
gap: 7px;
|
|
2998
|
+
color: #cbd5e1;
|
|
2999
|
+
font-size: 12px;
|
|
3000
|
+
line-height: 1;
|
|
3001
|
+
white-space: nowrap;
|
|
3002
|
+
}
|
|
3003
|
+
.ai-game-assets-designer__inline-checkbox input {
|
|
3004
|
+
width: auto;
|
|
3005
|
+
margin: 0;
|
|
3006
|
+
}
|
|
3007
|
+
.ai-game-assets-designer__audio-editor-transport button {
|
|
3008
|
+
width: 36px;
|
|
3009
|
+
height: 32px;
|
|
3010
|
+
border: 1px solid #58657a;
|
|
3011
|
+
border-radius: 6px;
|
|
3012
|
+
background: #273142;
|
|
3013
|
+
color: #fff;
|
|
3014
|
+
padding: 0;
|
|
3015
|
+
font: inherit;
|
|
3016
|
+
font-size: 14px;
|
|
3017
|
+
cursor: pointer;
|
|
3018
|
+
}
|
|
3019
|
+
.ai-game-assets-designer__audio-editor-time {
|
|
3020
|
+
color: #cbd5e1;
|
|
3021
|
+
font-size: 12px;
|
|
3022
|
+
font-variant-numeric: tabular-nums;
|
|
3023
|
+
text-align: right;
|
|
3024
|
+
}
|
|
3025
|
+
.ai-game-assets-designer__audio-editor-fields {
|
|
3026
|
+
display: grid;
|
|
3027
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
3028
|
+
gap: 8px;
|
|
3029
|
+
margin-top: 10px;
|
|
3030
|
+
}
|
|
3031
|
+
.ai-game-assets-designer__audio-editor-fields .ai-game-assets-designer__field {
|
|
3032
|
+
margin-bottom: 0;
|
|
3033
|
+
}
|
|
3034
|
+
.ai-game-assets-designer__audio-editor-fields input[type="range"] {
|
|
3035
|
+
width: 100%;
|
|
3036
|
+
}
|
|
3037
|
+
.ai-game-assets-designer__audio-editor-hint {
|
|
3038
|
+
margin-top: 10px;
|
|
3039
|
+
color: #94a3b8;
|
|
3040
|
+
font-size: 12px;
|
|
3041
|
+
line-height: 1.35;
|
|
3042
|
+
}
|
|
3043
|
+
.ai-game-assets-designer__modal-actions {
|
|
3044
|
+
display: flex;
|
|
3045
|
+
justify-content: flex-end;
|
|
3046
|
+
gap: 8px;
|
|
3047
|
+
margin-top: 14px;
|
|
3048
|
+
}
|
|
3049
|
+
.ai-game-assets-designer__modal-actions button {
|
|
3050
|
+
border: 1px solid #58657a;
|
|
3051
|
+
border-radius: 6px;
|
|
3052
|
+
background: #273142;
|
|
3053
|
+
color: #fff;
|
|
3054
|
+
padding: 9px 11px;
|
|
3055
|
+
font: inherit;
|
|
3056
|
+
cursor: pointer;
|
|
3057
|
+
}
|
|
3058
|
+
.ai-game-assets-designer__modal-actions button:disabled {
|
|
3059
|
+
cursor: not-allowed;
|
|
3060
|
+
opacity: 0.45;
|
|
3061
|
+
}
|
|
3062
|
+
.ai-game-assets-designer__touchup {
|
|
3063
|
+
position: fixed;
|
|
3064
|
+
inset: 0;
|
|
3065
|
+
z-index: 1;
|
|
3066
|
+
display: grid;
|
|
3067
|
+
grid-template-rows: auto auto minmax(0, 1fr);
|
|
3068
|
+
gap: 10px;
|
|
3069
|
+
padding: 14px;
|
|
3070
|
+
background: #0b0f16;
|
|
3071
|
+
color: #f5f7fb;
|
|
3072
|
+
}
|
|
3073
|
+
.ai-game-assets-designer__touchup-header {
|
|
3074
|
+
display: flex;
|
|
3075
|
+
align-items: center;
|
|
3076
|
+
gap: 12px;
|
|
3077
|
+
min-height: 34px;
|
|
3078
|
+
}
|
|
3079
|
+
.ai-game-assets-designer__touchup-title {
|
|
3080
|
+
font-weight: 700;
|
|
3081
|
+
font-size: 15px;
|
|
3082
|
+
}
|
|
3083
|
+
.ai-game-assets-designer__touchup-dirty {
|
|
3084
|
+
border: 1px solid #fbbf24;
|
|
3085
|
+
border-radius: 999px;
|
|
3086
|
+
color: #fde68a;
|
|
3087
|
+
padding: 3px 8px;
|
|
3088
|
+
font-size: 12px;
|
|
3089
|
+
}
|
|
3090
|
+
.ai-game-assets-designer__touchup-close {
|
|
3091
|
+
margin-left: auto;
|
|
3092
|
+
width: 32px;
|
|
3093
|
+
height: 32px;
|
|
3094
|
+
border: 1px solid #58657a;
|
|
3095
|
+
border-radius: 6px;
|
|
3096
|
+
background: #273142;
|
|
3097
|
+
color: #fff;
|
|
3098
|
+
font: inherit;
|
|
3099
|
+
cursor: pointer;
|
|
3100
|
+
}
|
|
3101
|
+
.ai-game-assets-designer__touchup-toolbar {
|
|
3102
|
+
display: flex;
|
|
3103
|
+
align-items: center;
|
|
3104
|
+
gap: 8px;
|
|
3105
|
+
flex-wrap: wrap;
|
|
3106
|
+
padding: 8px;
|
|
3107
|
+
border: 1px solid #303949;
|
|
3108
|
+
border-radius: 8px;
|
|
3109
|
+
background: #141820;
|
|
3110
|
+
}
|
|
3111
|
+
.ai-game-assets-designer__touchup-toolbar button {
|
|
3112
|
+
border: 1px solid #58657a;
|
|
3113
|
+
border-radius: 6px;
|
|
3114
|
+
background: #273142;
|
|
3115
|
+
color: #fff;
|
|
3116
|
+
padding: 7px 9px;
|
|
3117
|
+
font: inherit;
|
|
3118
|
+
font-size: 12px;
|
|
3119
|
+
cursor: pointer;
|
|
3120
|
+
}
|
|
3121
|
+
.ai-game-assets-designer__touchup-toolbar button.is-active {
|
|
3122
|
+
border-color: #6ed3ff;
|
|
3123
|
+
background: #17425a;
|
|
3124
|
+
}
|
|
3125
|
+
.ai-game-assets-designer__touchup-toolbar button.is-primary {
|
|
3126
|
+
border-color: #86efac;
|
|
3127
|
+
background: #14532d;
|
|
3128
|
+
}
|
|
3129
|
+
.ai-game-assets-designer__touchup-toolbar button:disabled {
|
|
3130
|
+
cursor: not-allowed;
|
|
3131
|
+
opacity: 0.45;
|
|
3132
|
+
}
|
|
3133
|
+
.ai-game-assets-designer__touchup-split {
|
|
3134
|
+
position: relative;
|
|
3135
|
+
display: inline-flex;
|
|
3136
|
+
align-items: stretch;
|
|
3137
|
+
}
|
|
3138
|
+
.ai-game-assets-designer__touchup-split > button:first-child {
|
|
3139
|
+
border-top-right-radius: 0;
|
|
3140
|
+
border-bottom-right-radius: 0;
|
|
3141
|
+
}
|
|
3142
|
+
.ai-game-assets-designer__touchup-toolbar .ai-game-assets-designer__touchup-split-arrow {
|
|
3143
|
+
min-width: 26px;
|
|
3144
|
+
padding: 7px 6px;
|
|
3145
|
+
border-left: 0;
|
|
3146
|
+
border-top-left-radius: 0;
|
|
3147
|
+
border-bottom-left-radius: 0;
|
|
3148
|
+
}
|
|
3149
|
+
.ai-game-assets-designer__touchup-split-arrow::before {
|
|
3150
|
+
content: "";
|
|
3151
|
+
display: block;
|
|
3152
|
+
width: 0;
|
|
3153
|
+
height: 0;
|
|
3154
|
+
margin: 0 auto;
|
|
3155
|
+
border-left: 5px solid transparent;
|
|
3156
|
+
border-right: 5px solid transparent;
|
|
3157
|
+
border-top: 6px solid currentColor;
|
|
3158
|
+
}
|
|
3159
|
+
.ai-game-assets-designer__touchup-brush-menu {
|
|
3160
|
+
position: absolute;
|
|
3161
|
+
top: calc(100% + 6px);
|
|
3162
|
+
left: 0;
|
|
3163
|
+
z-index: 2;
|
|
3164
|
+
width: 210px;
|
|
3165
|
+
display: grid;
|
|
3166
|
+
gap: 10px;
|
|
3167
|
+
padding: 10px;
|
|
3168
|
+
border: 1px solid #384251;
|
|
3169
|
+
border-radius: 8px;
|
|
3170
|
+
background: #141820;
|
|
3171
|
+
box-shadow: 0 16px 38px rgba(0, 0, 0, 0.42);
|
|
3172
|
+
}
|
|
3173
|
+
.ai-game-assets-designer__touchup-brush-size {
|
|
3174
|
+
display: grid;
|
|
3175
|
+
grid-template-columns: auto minmax(0, 1fr) 28px;
|
|
3176
|
+
align-items: center;
|
|
3177
|
+
gap: 8px;
|
|
3178
|
+
color: #cbd5e1;
|
|
3179
|
+
font-size: 12px;
|
|
3180
|
+
}
|
|
3181
|
+
.ai-game-assets-designer__touchup-brush-size input {
|
|
3182
|
+
width: 100%;
|
|
3183
|
+
}
|
|
3184
|
+
.ai-game-assets-designer__touchup-brush-size span {
|
|
3185
|
+
color: #f8fafc;
|
|
3186
|
+
font-variant-numeric: tabular-nums;
|
|
3187
|
+
text-align: right;
|
|
3188
|
+
}
|
|
3189
|
+
.ai-game-assets-designer__touchup-color {
|
|
3190
|
+
display: grid;
|
|
3191
|
+
grid-template-columns: 38px auto 58px;
|
|
3192
|
+
align-items: center;
|
|
3193
|
+
gap: 6px;
|
|
3194
|
+
color: #cbd5e1;
|
|
3195
|
+
font-size: 12px;
|
|
3196
|
+
}
|
|
3197
|
+
.ai-game-assets-designer__touchup-color input[type="color"] {
|
|
3198
|
+
width: 38px;
|
|
3199
|
+
height: 30px;
|
|
3200
|
+
border: 1px solid #58657a;
|
|
3201
|
+
border-radius: 6px;
|
|
3202
|
+
background: #101319;
|
|
3203
|
+
padding: 2px;
|
|
3204
|
+
}
|
|
3205
|
+
.ai-game-assets-designer__touchup-color input[type="number"] {
|
|
3206
|
+
width: 58px;
|
|
3207
|
+
border: 1px solid #3a4352;
|
|
3208
|
+
border-radius: 6px;
|
|
3209
|
+
background: #101319;
|
|
3210
|
+
color: #f5f7fb;
|
|
3211
|
+
padding: 6px;
|
|
3212
|
+
font: inherit;
|
|
3213
|
+
font-size: 12px;
|
|
3214
|
+
}
|
|
3215
|
+
.ai-game-assets-designer__touchup-workspace {
|
|
3216
|
+
min-height: 0;
|
|
3217
|
+
display: grid;
|
|
3218
|
+
grid-template-columns: minmax(0, 1fr) 190px;
|
|
3219
|
+
gap: 12px;
|
|
3220
|
+
}
|
|
3221
|
+
.ai-game-assets-designer__touchup-canvas-wrap {
|
|
3222
|
+
position: relative;
|
|
3223
|
+
min-height: 0;
|
|
3224
|
+
overflow: auto;
|
|
3225
|
+
border: 1px solid #303949;
|
|
3226
|
+
border-radius: 8px;
|
|
3227
|
+
background-color: #111827;
|
|
3228
|
+
background-image:
|
|
3229
|
+
linear-gradient(45deg, rgba(255, 255, 255, 0.08) 25%, transparent 25%),
|
|
3230
|
+
linear-gradient(-45deg, rgba(255, 255, 255, 0.08) 25%, transparent 25%),
|
|
3231
|
+
linear-gradient(45deg, transparent 75%, rgba(255, 255, 255, 0.08) 75%),
|
|
3232
|
+
linear-gradient(-45deg, transparent 75%, rgba(255, 255, 255, 0.08) 75%);
|
|
3233
|
+
background-position: 0 0, 0 8px, 8px -8px, -8px 0;
|
|
3234
|
+
background-size: 16px 16px;
|
|
3235
|
+
touch-action: none;
|
|
3236
|
+
}
|
|
3237
|
+
.ai-game-assets-designer__touchup-canvas {
|
|
3238
|
+
display: block;
|
|
3239
|
+
margin: 24px;
|
|
3240
|
+
image-rendering: pixelated;
|
|
3241
|
+
cursor: crosshair;
|
|
3242
|
+
touch-action: none;
|
|
3243
|
+
}
|
|
3244
|
+
.ai-game-assets-designer__touchup-selection {
|
|
3245
|
+
position: absolute;
|
|
3246
|
+
pointer-events: none;
|
|
3247
|
+
border: 1px dashed #f8fafc;
|
|
3248
|
+
box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.9);
|
|
3249
|
+
transform: translate(24px, 24px);
|
|
3250
|
+
}
|
|
3251
|
+
.ai-game-assets-designer__touchup-side {
|
|
3252
|
+
min-height: 0;
|
|
3253
|
+
display: grid;
|
|
3254
|
+
grid-template-rows: auto auto minmax(0, 1fr);
|
|
3255
|
+
gap: 10px;
|
|
3256
|
+
}
|
|
3257
|
+
.ai-game-assets-designer__touchup-panel {
|
|
3258
|
+
display: grid;
|
|
3259
|
+
gap: 8px;
|
|
3260
|
+
padding: 10px;
|
|
3261
|
+
border: 1px solid #303949;
|
|
3262
|
+
border-radius: 8px;
|
|
3263
|
+
background: #141820;
|
|
3264
|
+
color: #cbd5e1;
|
|
3265
|
+
font-size: 12px;
|
|
3266
|
+
}
|
|
3267
|
+
.ai-game-assets-designer__touchup-panel canvas {
|
|
3268
|
+
width: 100%;
|
|
3269
|
+
max-height: 160px;
|
|
3270
|
+
object-fit: contain;
|
|
3271
|
+
border: 1px solid #263243;
|
|
3272
|
+
border-radius: 6px;
|
|
3273
|
+
background: #0f1218;
|
|
3274
|
+
image-rendering: pixelated;
|
|
3275
|
+
}
|
|
3276
|
+
@media (max-width: 760px) {
|
|
3277
|
+
.ai-game-assets-designer__touchup-workspace {
|
|
3278
|
+
grid-template-columns: 1fr;
|
|
3279
|
+
}
|
|
3280
|
+
.ai-game-assets-designer__touchup-side {
|
|
3281
|
+
grid-template-columns: 1fr 1fr;
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
.ai-game-assets-designer__style-drop {
|
|
3285
|
+
display: grid;
|
|
3286
|
+
place-items: center;
|
|
3287
|
+
min-height: 90px;
|
|
3288
|
+
margin-bottom: 10px;
|
|
3289
|
+
border: 1px dashed #58657a;
|
|
3290
|
+
border-radius: 6px;
|
|
3291
|
+
background: #0f1218;
|
|
3292
|
+
color: #b9c1cf;
|
|
3293
|
+
font-size: 13px;
|
|
3294
|
+
}
|
|
3295
|
+
.ai-game-assets-designer__style-drop.is-dragging {
|
|
3296
|
+
border-color: #6ed3ff;
|
|
3297
|
+
color: #dbeafe;
|
|
3298
|
+
}
|
|
3299
|
+
.ai-game-assets-designer__style-drop + button {
|
|
3300
|
+
border: 1px solid #58657a;
|
|
3301
|
+
border-radius: 6px;
|
|
3302
|
+
background: #273142;
|
|
3303
|
+
color: #fff;
|
|
3304
|
+
padding: 8px 10px;
|
|
3305
|
+
font: inherit;
|
|
3306
|
+
cursor: pointer;
|
|
3307
|
+
}
|
|
3308
|
+
.ai-game-assets-designer__style-images {
|
|
3309
|
+
display: grid;
|
|
3310
|
+
gap: 8px;
|
|
3311
|
+
margin-top: 10px;
|
|
3312
|
+
}
|
|
3313
|
+
.ai-game-assets-designer__style-image {
|
|
3314
|
+
display: grid;
|
|
3315
|
+
grid-template-columns: 48px minmax(0, 1fr) auto;
|
|
3316
|
+
align-items: center;
|
|
3317
|
+
gap: 8px;
|
|
3318
|
+
padding: 6px;
|
|
3319
|
+
border: 1px solid #384251;
|
|
3320
|
+
border-radius: 6px;
|
|
3321
|
+
background: #0f1218;
|
|
3322
|
+
font-size: 12px;
|
|
3323
|
+
}
|
|
3324
|
+
.ai-game-assets-designer__style-image img {
|
|
3325
|
+
width: 48px;
|
|
3326
|
+
height: 48px;
|
|
3327
|
+
object-fit: contain;
|
|
3328
|
+
}
|
|
3329
|
+
.ai-game-assets-designer__style-image span {
|
|
3330
|
+
overflow-wrap: anywhere;
|
|
3331
|
+
}
|
|
3332
|
+
.ai-game-assets-designer__style-image button {
|
|
3333
|
+
border: 0;
|
|
3334
|
+
background: transparent;
|
|
3335
|
+
color: #fca5a5;
|
|
3336
|
+
cursor: pointer;
|
|
3337
|
+
}
|
|
3338
|
+
`;
|
|
3339
|
+
document.head.append(style);
|
|
3340
|
+
}
|
|
3341
|
+
//# sourceMappingURL=designer-support.js.map
|