@ai-game-assets/dev 0.8.3 → 0.9.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.
@@ -1,48 +1,5 @@
1
1
  import { PNG } from "pngjs";
2
2
  import sharp from "sharp";
3
- const DEFAULT_CHROMA_KEY = { red: 255, green: 0, blue: 255 };
4
- const CHROMA_KEY_CANDIDATES = [
5
- DEFAULT_CHROMA_KEY,
6
- { red: 0, green: 255, blue: 0 },
7
- { red: 0, green: 255, blue: 255 },
8
- { red: 255, green: 255, blue: 0 },
9
- { red: 0, green: 0, blue: 255 }
10
- ];
11
- const CHROMA_MATCH_TOLERANCE = 120;
12
- const CHROMA_EDGE_TOLERANCE = 170;
13
- const CHROMA_EDGE_FILL_TOLERANCE = 150;
14
- const STRICT_CHROMA_MATCH_TOLERANCE = 90;
15
- const COHERENT_EDGE_MATTE_TOLERANCE = 96;
16
- const COHERENT_EDGE_MATTE_MIN_COVERAGE = 0.55;
17
- const OPAQUE_EDGE_ALPHA_THRESHOLD = 224;
18
- export function selectChromaKey(request) {
19
- const samples = [...(request.references ?? []), ...(request.styleReferences ?? [])]
20
- .flatMap((reference) => referenceColorSamples(reference));
21
- const prompt = [
22
- request.prompt,
23
- request.asset.prompt,
24
- request.stylePrompt,
25
- ...(request.asset.tileset?.tiles?.map((tile) => tile.prompt) ?? [])
26
- ].filter(Boolean).join(" ").toLowerCase();
27
- if (!samples.length) {
28
- return CHROMA_KEY_CANDIDATES.find((candidate) => !promptMentionsChromaFamily(prompt, candidate)) ??
29
- DEFAULT_CHROMA_KEY;
30
- }
31
- return CHROMA_KEY_CANDIDATES
32
- .filter((candidate) => !promptMentionsChromaFamily(prompt, candidate))
33
- .map((candidate) => ({
34
- candidate,
35
- score: Math.min(...samples.map((sample) => colorDistance(candidate, sample)))
36
- }))
37
- .sort((left, right) => right.score - left.score)[0]?.candidate ??
38
- CHROMA_KEY_CANDIDATES
39
- .map((candidate) => ({
40
- candidate,
41
- score: Math.min(...samples.map((sample) => colorDistance(candidate, sample)))
42
- }))
43
- .sort((left, right) => right.score - left.score)[0]?.candidate ??
44
- DEFAULT_CHROMA_KEY;
45
- }
46
3
  export function referenceLockPromptLines(references) {
47
4
  const analyses = references
48
5
  .map((reference) => analyzeReferenceImage(reference))
@@ -70,28 +27,6 @@ export function referenceLockPromptLines(references) {
70
27
  return lines;
71
28
  });
72
29
  }
73
- export function referenceColorSamples(reference) {
74
- if (reference.mimeType !== "image/png")
75
- return [];
76
- let png;
77
- try {
78
- png = PNG.sync.read(Buffer.from(reference.image));
79
- }
80
- catch {
81
- return [];
82
- }
83
- const bins = new Map();
84
- for (let offset = 0; offset < png.data.length; offset += 4) {
85
- const alpha = png.data[offset + 3] ?? 255;
86
- if (alpha < 64)
87
- continue;
88
- const red = png.data[offset] ?? 0;
89
- const green = png.data[offset + 1] ?? 0;
90
- const blue = png.data[offset + 2] ?? 0;
91
- addColorBin(bins, red, green, blue);
92
- }
93
- return topColorBinValues(bins, 8);
94
- }
95
30
  export function analyzeReferenceImage(reference) {
96
31
  if (reference.mimeType !== "image/png")
97
32
  return undefined;
@@ -116,8 +51,6 @@ export function analyzeReferenceImage(reference) {
116
51
  const red = png.data[offset] ?? 0;
117
52
  const green = png.data[offset + 1] ?? 0;
118
53
  const blue = png.data[offset + 2] ?? 0;
119
- if (isChromaRgb(red, green, blue))
120
- continue;
121
54
  visiblePixels += 1;
122
55
  const max = Math.max(red, green, blue);
123
56
  const min = Math.min(red, green, blue);
@@ -181,9 +114,6 @@ export function topColorBinValues(bins, count) {
181
114
  export function quantizeColor(value) {
182
115
  return Math.round(value / 32) * 32;
183
116
  }
184
- export function isChromaRgb(red, green, blue) {
185
- return CHROMA_KEY_CANDIDATES.some((chromaKey) => (colorDistance({ red, green, blue }, chromaKey) < 90));
186
- }
187
117
  export function isBrightSaturatedColor(red, green, blue) {
188
118
  const max = Math.max(red, green, blue);
189
119
  const min = Math.min(red, green, blue);
@@ -210,16 +140,6 @@ export function colorName(red, green, blue) {
210
140
  return blue - red > 40 ? "blue" : "purple";
211
141
  return "color";
212
142
  }
213
- export function colorDistance(left, right) {
214
- const redDelta = left.red - right.red;
215
- const greenDelta = left.green - right.green;
216
- const blueDelta = left.blue - right.blue;
217
- return Math.sqrt(redDelta * redDelta + greenDelta * greenDelta + blueDelta * blueDelta);
218
- }
219
- export function hexColor(color) {
220
- const channel = (value) => value.toString(16).padStart(2, "0");
221
- return `#${channel(color.red)}${channel(color.green)}${channel(color.blue)}`;
222
- }
223
143
  export function rgbColor(color) {
224
144
  return `rgb(${color.red}, ${color.green}, ${color.blue})`;
225
145
  }
@@ -232,8 +152,8 @@ export function variationDirectionPromptLine(index) {
232
152
  ];
233
153
  return variants[index % variants.length];
234
154
  }
235
- export function shouldRequestRgbaPng(request, context) {
236
- if (!context.model.startsWith("gpt-image-2") || context.outputFormat !== "png") {
155
+ export function shouldRequestTransparency(request, context) {
156
+ if (context.outputFormat === "jpeg") {
237
157
  return false;
238
158
  }
239
159
  if (context.requestedBackground === "opaque") {
@@ -254,270 +174,6 @@ export function resolveRequestedBackground(request, options) {
254
174
  }
255
175
  return options.background ?? "transparent";
256
176
  }
257
- export function shouldPostprocessTransparency(request, context) {
258
- return shouldRequestRgbaPng(request, context);
259
- }
260
- export function removeChromaBackground(image, chromaKey) {
261
- const png = PNG.sync.read(Buffer.from(image));
262
- const backgroundRemoval = detectBackgroundRemoval(png, chromaKey);
263
- if (!backgroundRemoval) {
264
- return Buffer.from(image);
265
- }
266
- removeDetectedBackground(png, backgroundRemoval);
267
- return PNG.sync.write(png);
268
- }
269
- export function removeTilesetChromaBackground(image, tileset, chromaKey) {
270
- const png = PNG.sync.read(Buffer.from(image));
271
- const tileCapacity = tileset.columns * tileset.rows;
272
- const tileCount = Math.min(tileset.tileCount ?? tileCapacity, tileCapacity);
273
- const cellPixels = new Uint8Array(png.width * png.height);
274
- for (let index = 0; index < tileCapacity; index += 1) {
275
- const bounds = scaledTilesetCellBounds(png, tileset, index);
276
- if (!bounds)
277
- continue;
278
- markPngRect(cellPixels, png.width, bounds.x, bounds.y, bounds.width, bounds.height);
279
- if (index >= tileCount) {
280
- clearPngRect(png, bounds.x, bounds.y, bounds.width, bounds.height);
281
- continue;
282
- }
283
- const cell = copyPngRect(png, bounds.x, bounds.y, bounds.width, bounds.height);
284
- if (removeKnownChromaPixels(cell, chromaKey)) {
285
- pastePngRect(png, cell, bounds.x, bounds.y);
286
- }
287
- }
288
- clearUnmarkedPngPixels(png, cellPixels);
289
- return PNG.sync.write(png);
290
- }
291
- function markPngRect(pixels, imageWidth, x, y, width, height) {
292
- for (let localY = 0; localY < height; localY += 1) {
293
- const start = (y + localY) * imageWidth + x;
294
- pixels.fill(1, start, start + width);
295
- }
296
- }
297
- function clearUnmarkedPngPixels(png, markedPixels) {
298
- for (let index = 0; index < markedPixels.length; index += 1) {
299
- if (markedPixels[index])
300
- continue;
301
- const offset = index * 4;
302
- png.data[offset] = 0;
303
- png.data[offset + 1] = 0;
304
- png.data[offset + 2] = 0;
305
- png.data[offset + 3] = 0;
306
- }
307
- }
308
- function removeDetectedBackground(png, backgroundRemoval) {
309
- const width = png.width;
310
- const height = png.height;
311
- const visited = new Uint8Array(width * height);
312
- const transparent = new Uint8Array(width * height);
313
- const queue = [];
314
- let queueCursor = 0;
315
- const enqueue = (x, y) => {
316
- if (x < 0 || y < 0 || x >= width || y >= height)
317
- return;
318
- const index = y * width + x;
319
- if (visited[index])
320
- return;
321
- visited[index] = 1;
322
- if (isRemovableEdgeBackgroundPixel(png, index, backgroundRemoval)) {
323
- queue.push(index);
324
- }
325
- };
326
- for (let x = 0; x < width; x += 1) {
327
- enqueue(x, 0);
328
- enqueue(x, height - 1);
329
- }
330
- for (let y = 1; y < height - 1; y += 1) {
331
- enqueue(0, y);
332
- enqueue(width - 1, y);
333
- }
334
- while (queueCursor < queue.length) {
335
- const index = queue[queueCursor];
336
- queueCursor += 1;
337
- transparent[index] = 1;
338
- const x = index % width;
339
- const y = Math.floor(index / width);
340
- enqueue(x + 1, y);
341
- enqueue(x - 1, y);
342
- enqueue(x, y + 1);
343
- enqueue(x, y - 1);
344
- }
345
- for (let index = 0; index < transparent.length; index += 1) {
346
- if (transparent[index] ||
347
- (backgroundRemoval.kind === "chroma" &&
348
- backgroundRemoval.removeDisconnected &&
349
- isHighConfidenceChromaPixel(png, index, backgroundRemoval.chromaKey))) {
350
- transparent[index] = 1;
351
- setAlpha(png, index, 0);
352
- }
353
- }
354
- featherBackgroundEdges(png, transparent, backgroundRemoval);
355
- }
356
- function removeKnownChromaPixels(png, chromaKey) {
357
- const transparent = new Uint8Array(png.width * png.height);
358
- let found = false;
359
- for (let index = 0; index < transparent.length; index += 1) {
360
- if (isChromaPixel(png, index, CHROMA_MATCH_TOLERANCE, chromaKey) ||
361
- isKeyTintedPixel(rgbAt(png, index), chromaKey, 0.86)) {
362
- transparent[index] = 1;
363
- setAlpha(png, index, 0);
364
- found = true;
365
- }
366
- }
367
- if (found) {
368
- featherBackgroundEdges(png, transparent, {
369
- kind: "chroma",
370
- chromaKey,
371
- removeDisconnected: true
372
- });
373
- }
374
- return found;
375
- }
376
- function scaledTilesetCellBounds(png, tileset, index) {
377
- const margin = tileset.margin ?? 0;
378
- const spacing = tileset.spacing ?? 0;
379
- const sheetWidth = margin * 2 + tileset.columns * tileset.tileWidth +
380
- Math.max(0, tileset.columns - 1) * spacing;
381
- const sheetHeight = margin * 2 + tileset.rows * tileset.tileHeight +
382
- Math.max(0, tileset.rows - 1) * spacing;
383
- if (sheetWidth <= 0 || sheetHeight <= 0 || index < 0 || index >= tileset.columns * tileset.rows) {
384
- return undefined;
385
- }
386
- const column = index % tileset.columns;
387
- const row = Math.floor(index / tileset.columns);
388
- const sourceX = margin + column * (tileset.tileWidth + spacing);
389
- const sourceY = margin + row * (tileset.tileHeight + spacing);
390
- const x = Math.round((sourceX / sheetWidth) * png.width);
391
- const y = Math.round((sourceY / sheetHeight) * png.height);
392
- const right = Math.round(((sourceX + tileset.tileWidth) / sheetWidth) * png.width);
393
- const bottom = Math.round(((sourceY + tileset.tileHeight) / sheetHeight) * png.height);
394
- return {
395
- x,
396
- y,
397
- width: Math.max(1, right - x),
398
- height: Math.max(1, bottom - y)
399
- };
400
- }
401
- function copyPngRect(png, x, y, width, height) {
402
- const target = new PNG({ width, height });
403
- PNG.bitblt(png, target, x, y, width, height, 0, 0);
404
- return target;
405
- }
406
- function pastePngRect(png, source, x, y) {
407
- PNG.bitblt(source, png, 0, 0, source.width, source.height, x, y);
408
- }
409
- export function detectBackgroundRemoval(png, chromaKey) {
410
- if (hasRequestedChromaKey(png, chromaKey)) {
411
- return {
412
- kind: "chroma",
413
- chromaKey,
414
- removeDisconnected: hasRequestedChromaKeyOnEdges(png, chromaKey)
415
- };
416
- }
417
- const matteColor = detectNeutralEdgeMatte(png);
418
- if (matteColor) {
419
- return { kind: "edge-matte", matteColor };
420
- }
421
- const coherentMatteColor = detectCoherentEdgeMatte(png);
422
- return coherentMatteColor
423
- ? { kind: "coherent-edge-matte", matteColor: coherentMatteColor }
424
- : undefined;
425
- }
426
- export function hasRequestedChromaKey(png, chromaKey) {
427
- if (hasRequestedChromaKeyOnEdges(png, chromaKey))
428
- return true;
429
- let matches = 0;
430
- const threshold = Math.max(256, Math.floor(png.width * png.height * 0.005));
431
- for (let index = 0; index < png.width * png.height; index += 1) {
432
- if (isChromaPixel(png, index, STRICT_CHROMA_MATCH_TOLERANCE, chromaKey) ||
433
- isKeyTintedPixel(rgbAt(png, index), chromaKey, 0.8)) {
434
- matches += 1;
435
- if (matches >= threshold)
436
- return true;
437
- }
438
- }
439
- return false;
440
- }
441
- export function hasRequestedChromaKeyOnEdges(png, chromaKey) {
442
- const edgeStats = sampleImageEdges(png, (index) => (isChromaPixel(png, index, STRICT_CHROMA_MATCH_TOLERANCE, chromaKey) ||
443
- isKeyTintedPixel(rgbAt(png, index), chromaKey, 0.8)));
444
- return edgeStats.matches / edgeStats.total >= 0.18;
445
- }
446
- export function detectNeutralEdgeMatte(png) {
447
- let edgePixelCount = 0;
448
- let neutralEdgePixelCount = 0;
449
- let redTotal = 0;
450
- let greenTotal = 0;
451
- let blueTotal = 0;
452
- sampleImageEdges(png, (index) => {
453
- const color = rgbAt(png, index);
454
- edgePixelCount += 1;
455
- if (isNeutralMatteCandidate(color)) {
456
- neutralEdgePixelCount += 1;
457
- redTotal += color.red;
458
- greenTotal += color.green;
459
- blueTotal += color.blue;
460
- }
461
- return false;
462
- });
463
- if (neutralEdgePixelCount === 0 ||
464
- neutralEdgePixelCount / edgePixelCount < 0.65 ||
465
- neutralCornerCount(png) < 3) {
466
- return undefined;
467
- }
468
- const matteColor = {
469
- red: Math.round(redTotal / neutralEdgePixelCount),
470
- green: Math.round(greenTotal / neutralEdgePixelCount),
471
- blue: Math.round(blueTotal / neutralEdgePixelCount)
472
- };
473
- const matteStats = sampleImageEdges(png, (index) => (isNeutralMattePixel(rgbAt(png, index), matteColor)));
474
- return matteStats.matches / matteStats.total >= 0.55 ? matteColor : undefined;
475
- }
476
- export function detectCoherentEdgeMatte(png) {
477
- const corners = [
478
- { x: 0, y: 0 },
479
- { x: png.width - 1, y: 0 },
480
- { x: 0, y: png.height - 1 },
481
- { x: png.width - 1, y: png.height - 1 }
482
- ].map(({ x, y }) => y * png.width + x)
483
- .filter((index) => alphaAt(png, index) >= OPAQUE_EDGE_ALPHA_THRESHOLD)
484
- .map((index) => rgbAt(png, index));
485
- if (corners.length < 3)
486
- return undefined;
487
- const cornerCluster = corners
488
- .map((seed) => corners.filter((color) => (colorDistance(color, seed) <= COHERENT_EDGE_MATTE_TOLERANCE)))
489
- .sort((left, right) => right.length - left.length)[0];
490
- if (!cornerCluster || cornerCluster.length < 3)
491
- return undefined;
492
- const cornerColor = averageColors(cornerCluster);
493
- const matchingEdgeColors = [];
494
- const edgeStats = sampleImageEdges(png, (index) => {
495
- if (alphaAt(png, index) < OPAQUE_EDGE_ALPHA_THRESHOLD)
496
- return false;
497
- const color = rgbAt(png, index);
498
- const matches = colorDistance(color, cornerColor) <= COHERENT_EDGE_MATTE_TOLERANCE;
499
- if (matches)
500
- matchingEdgeColors.push(color);
501
- return matches;
502
- });
503
- if (matchingEdgeColors.length === 0 ||
504
- edgeStats.matches / edgeStats.total < COHERENT_EDGE_MATTE_MIN_COVERAGE) {
505
- return undefined;
506
- }
507
- const matteColor = averageColors(matchingEdgeColors);
508
- const refinedStats = sampleImageEdges(png, (index) => (alphaAt(png, index) >= OPAQUE_EDGE_ALPHA_THRESHOLD &&
509
- colorDistance(rgbAt(png, index), matteColor) <= COHERENT_EDGE_MATTE_TOLERANCE));
510
- return refinedStats.matches / refinedStats.total >= COHERENT_EDGE_MATTE_MIN_COVERAGE
511
- ? matteColor
512
- : undefined;
513
- }
514
- function averageColors(colors) {
515
- return {
516
- red: Math.round(colors.reduce((sum, color) => sum + color.red, 0) / colors.length),
517
- green: Math.round(colors.reduce((sum, color) => sum + color.green, 0) / colors.length),
518
- blue: Math.round(colors.reduce((sum, color) => sum + color.blue, 0) / colors.length)
519
- };
520
- }
521
177
  export function resizePngToDimensions(image, dimensions) {
522
178
  const source = PNG.sync.read(Buffer.from(image));
523
179
  if (source.width === dimensions.width && source.height === dimensions.height) {
@@ -541,6 +197,42 @@ export function resizePngToDimensions(image, dimensions) {
541
197
  }
542
198
  return PNG.sync.write(target);
543
199
  }
200
+ export async function composeSpriteSheetFrames(frameImages, dimensions, frameGrid) {
201
+ const margin = frameGrid.margin ?? 0;
202
+ const spacing = frameGrid.spacing ?? 0;
203
+ const frameCount = Math.min(frameGrid.frameCount ?? frameGrid.columns * frameGrid.rows, frameGrid.columns * frameGrid.rows);
204
+ if (frameImages.length !== frameCount) {
205
+ throw new Error(`Expected ${frameCount} generated sprite frames, received ${frameImages.length}.`);
206
+ }
207
+ const normalizedFrames = await Promise.all(frameImages.map((image) => (sharp(Buffer.from(image), { failOn: "error" })
208
+ .resize(frameGrid.frameWidth, frameGrid.frameHeight, {
209
+ fit: "fill",
210
+ kernel: sharp.kernel.nearest
211
+ })
212
+ .png()
213
+ .toBuffer())));
214
+ const composites = normalizedFrames.map((input, index) => {
215
+ const column = index % frameGrid.columns;
216
+ const row = Math.floor(index / frameGrid.columns);
217
+ const left = margin + column * (frameGrid.frameWidth + spacing);
218
+ const top = margin + row * (frameGrid.frameHeight + spacing);
219
+ if (left < 0 ||
220
+ top < 0 ||
221
+ left + frameGrid.frameWidth > dimensions.width ||
222
+ top + frameGrid.frameHeight > dimensions.height) {
223
+ throw new Error(`Sprite frame ${index + 1} lies outside the ${dimensions.width}x${dimensions.height} sheet.`);
224
+ }
225
+ return { input, left, top };
226
+ });
227
+ return sharp({
228
+ create: {
229
+ width: dimensions.width,
230
+ height: dimensions.height,
231
+ channels: 4,
232
+ background: { r: 0, g: 0, b: 0, alpha: 0 }
233
+ }
234
+ }).composite(composites).png().toBuffer();
235
+ }
544
236
  export async function resizeRasterToDimensions(image, dimensions, outputFormat) {
545
237
  const sourceImage = Buffer.from(image);
546
238
  const metadata = await sharp(sourceImage, {
@@ -557,6 +249,8 @@ export async function resizeRasterToDimensions(image, dimensions, outputFormat)
557
249
  fit: "fill",
558
250
  kernel: sharp.kernel.nearest
559
251
  });
252
+ if (outputFormat === "png")
253
+ return resized.png().toBuffer();
560
254
  return outputFormat === "webp"
561
255
  ? resized.webp({ quality: 100 }).toBuffer()
562
256
  : resized.jpeg({ quality: 95 }).toBuffer();
@@ -708,143 +402,4 @@ function copyShiftedPngRect(png, source, options) {
708
402
  }
709
403
  }
710
404
  }
711
- export function featherBackgroundEdges(png, transparent, backgroundRemoval) {
712
- const width = png.width;
713
- const height = png.height;
714
- for (let y = 0; y < height; y += 1) {
715
- for (let x = 0; x < width; x += 1) {
716
- const index = y * width + x;
717
- if (transparent[index] || !touchesTransparentNeighbor(transparent, width, height, x, y)) {
718
- continue;
719
- }
720
- const distance = backgroundDistance(png, index, backgroundRemoval);
721
- if (distance > CHROMA_EDGE_TOLERANCE) {
722
- continue;
723
- }
724
- const alpha = Math.max(0, Math.min(255, Math.round((distance / CHROMA_EDGE_TOLERANCE) * 255)));
725
- setAlpha(png, index, Math.min(alphaAt(png, index), alpha));
726
- }
727
- }
728
- }
729
- export function touchesTransparentNeighbor(transparent, width, height, x, y) {
730
- return ((x > 0 && transparent[y * width + x - 1] === 1) ||
731
- (x < width - 1 && transparent[y * width + x + 1] === 1) ||
732
- (y > 0 && transparent[(y - 1) * width + x] === 1) ||
733
- (y < height - 1 && transparent[(y + 1) * width + x] === 1));
734
- }
735
- export function isChromaPixel(png, index, tolerance, chromaKey) {
736
- return chromaDistance(png, index, chromaKey) <= tolerance;
737
- }
738
- export function isRemovableEdgeBackgroundPixel(png, index, backgroundRemoval) {
739
- if (backgroundRemoval.kind === "chroma") {
740
- return (isChromaPixel(png, index, CHROMA_EDGE_FILL_TOLERANCE, backgroundRemoval.chromaKey) ||
741
- isKeyTintedPixel(rgbAt(png, index), backgroundRemoval.chromaKey, 0.86));
742
- }
743
- if (backgroundRemoval.kind === "coherent-edge-matte") {
744
- return colorDistance(rgbAt(png, index), backgroundRemoval.matteColor) <=
745
- COHERENT_EDGE_MATTE_TOLERANCE;
746
- }
747
- return isNeutralMattePixel(rgbAt(png, index), backgroundRemoval.matteColor);
748
- }
749
- export function isHighConfidenceChromaPixel(png, index, chromaKey) {
750
- return (isChromaPixel(png, index, STRICT_CHROMA_MATCH_TOLERANCE, chromaKey) ||
751
- isKeyTintedPixel(rgbAt(png, index), chromaKey, 0.86));
752
- }
753
- export function isKeyTintedPixel(color, chromaKey, strength) {
754
- const keyChannels = [
755
- { color: color.red, key: chromaKey.red },
756
- { color: color.green, key: chromaKey.green },
757
- { color: color.blue, key: chromaKey.blue }
758
- ];
759
- const activeChannels = keyChannels.filter((channel) => channel.key > 128);
760
- const inactiveChannels = keyChannels.filter((channel) => channel.key <= 128);
761
- return (activeChannels.every((channel) => channel.color >= 150 * strength) &&
762
- inactiveChannels.every((channel) => channel.color <= 190 * (1.1 - strength)));
763
- }
764
- export function promptMentionsChromaFamily(prompt, chromaKey) {
765
- if (chromaKey.red === 255 && chromaKey.green === 0 && chromaKey.blue === 255) {
766
- return /\b(red|pink|purple|magenta|violet|crimson|fuchsia)\b/.test(prompt);
767
- }
768
- if (chromaKey.red === 0 && chromaKey.green === 255 && chromaKey.blue === 0) {
769
- return /\b(green|lime|emerald|neon green|chartreuse)\b/.test(prompt);
770
- }
771
- if (chromaKey.red === 0 && chromaKey.green === 255 && chromaKey.blue === 255) {
772
- return /\b(cyan|aqua|teal|turquoise|blue)\b/.test(prompt);
773
- }
774
- if (chromaKey.red === 255 && chromaKey.green === 255 && chromaKey.blue === 0) {
775
- return /\b(yellow|gold|golden|amber)\b/.test(prompt);
776
- }
777
- if (chromaKey.red === 0 && chromaKey.green === 0 && chromaKey.blue === 255) {
778
- return /\b(blue|navy|cobalt|azure|indigo|cyan)\b/.test(prompt);
779
- }
780
- return false;
781
- }
782
- export function chromaDistance(png, index, chromaKey) {
783
- return colorDistance(rgbAt(png, index), chromaKey);
784
- }
785
- export function backgroundDistance(png, index, backgroundRemoval) {
786
- if (backgroundRemoval.kind === "chroma") {
787
- return chromaDistance(png, index, backgroundRemoval.chromaKey);
788
- }
789
- return colorDistance(rgbAt(png, index), backgroundRemoval.matteColor);
790
- }
791
- export function isNeutralMattePixel(color, matteColor) {
792
- const max = Math.max(color.red, color.green, color.blue);
793
- const min = Math.min(color.red, color.green, color.blue);
794
- const saturation = max === 0 ? 0 : (max - min) / max;
795
- const luminance = relativeLuminance(color.red, color.green, color.blue);
796
- const matteLuminance = relativeLuminance(matteColor.red, matteColor.green, matteColor.blue);
797
- return (saturation <= 0.22 &&
798
- luminance >= 80 &&
799
- Math.abs(luminance - matteLuminance) <= 70 &&
800
- colorDistance(color, matteColor) <= 92);
801
- }
802
- export function isNeutralMatteCandidate(color) {
803
- const max = Math.max(color.red, color.green, color.blue);
804
- const min = Math.min(color.red, color.green, color.blue);
805
- const saturation = max === 0 ? 0 : (max - min) / max;
806
- const luminance = relativeLuminance(color.red, color.green, color.blue);
807
- return saturation <= 0.22 && luminance >= 75;
808
- }
809
- export function neutralCornerCount(png) {
810
- return [
811
- { x: 0, y: 0 },
812
- { x: png.width - 1, y: 0 },
813
- { x: 0, y: png.height - 1 },
814
- { x: png.width - 1, y: png.height - 1 }
815
- ].filter(({ x, y }) => isNeutralMatteCandidate(rgbAt(png, y * png.width + x))).length;
816
- }
817
- export function sampleImageEdges(png, predicate) {
818
- let total = 0;
819
- let matches = 0;
820
- const sample = (x, y) => {
821
- total += 1;
822
- if (predicate(y * png.width + x)) {
823
- matches += 1;
824
- }
825
- };
826
- for (let x = 0; x < png.width; x += 1) {
827
- sample(x, 0);
828
- sample(x, png.height - 1);
829
- }
830
- for (let y = 1; y < png.height - 1; y += 1) {
831
- sample(0, y);
832
- sample(png.width - 1, y);
833
- }
834
- return { total, matches };
835
- }
836
- export function rgbAt(png, index) {
837
- const offset = index * 4;
838
- return {
839
- red: png.data[offset] ?? 0,
840
- green: png.data[offset + 1] ?? 0,
841
- blue: png.data[offset + 2] ?? 0
842
- };
843
- }
844
- export function alphaAt(png, index) {
845
- return png.data[index * 4 + 3] ?? 255;
846
- }
847
- export function setAlpha(png, index, alpha) {
848
- png.data[index * 4 + 3] = alpha;
849
- }
850
405
  //# sourceMappingURL=provider-image-processing.js.map