sprite-strip 0.1.0 → 0.1.1
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/README.md +8 -0
- package/dist/index.cjs +24 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +24 -9
- package/dist/index.js.map +1 -1
- package/package.json +12 -7
package/README.md
CHANGED
|
@@ -97,3 +97,11 @@ Browser only — `prepareSprite` uses `Image`, `document.createElement('canvas')
|
|
|
97
97
|
`canvas.toDataURL`. `stripEdgeBackground` itself only calls `getImageData`/
|
|
98
98
|
`putImageData` on the context you pass it, so it could work with a non-DOM canvas
|
|
99
99
|
implementation, but that's untested and unsupported for now.
|
|
100
|
+
|
|
101
|
+
## Development
|
|
102
|
+
|
|
103
|
+
This repo is a mirror. sprite-strip is developed in the
|
|
104
|
+
[Fractaleyez](https://github.com/c0d3ster/Fractaleyez) monorepo under
|
|
105
|
+
`packages/sprite-strip` and pushed here with `git subtree`, so issues and PRs here are
|
|
106
|
+
welcome and get pulled back in. Locally: `pnpm install`, then `pnpm test`, `pnpm typecheck`,
|
|
107
|
+
`pnpm lint` and `pnpm build`.
|
package/dist/index.cjs
CHANGED
|
@@ -68,7 +68,16 @@ var isFlatImage = (data, pixelCount, coverage, quantLevels) => {
|
|
|
68
68
|
return (top1 + top2) / opaqueCount >= coverage;
|
|
69
69
|
};
|
|
70
70
|
var stripEdgeBackground = (ctx, width, height, options) => {
|
|
71
|
-
const opts = {
|
|
71
|
+
const opts = {
|
|
72
|
+
neutralChannelSpread: options?.neutralChannelSpread ?? DEFAULT_OPTIONS.neutralChannelSpread,
|
|
73
|
+
floodFillStepTolerance: options?.floodFillStepTolerance ?? DEFAULT_OPTIONS.floodFillStepTolerance,
|
|
74
|
+
floodFillMaxDrift: options?.floodFillMaxDrift ?? DEFAULT_OPTIONS.floodFillMaxDrift,
|
|
75
|
+
falloffRadiusPx: options?.falloffRadiusPx ?? DEFAULT_OPTIONS.falloffRadiusPx,
|
|
76
|
+
flatImageColorCoverage: options?.flatImageColorCoverage ?? DEFAULT_OPTIONS.flatImageColorCoverage,
|
|
77
|
+
flatImageColorQuantLevels: options?.flatImageColorQuantLevels ?? DEFAULT_OPTIONS.flatImageColorQuantLevels,
|
|
78
|
+
interiorBackgroundMatchTolerance: options?.interiorBackgroundMatchTolerance ?? DEFAULT_OPTIONS.interiorBackgroundMatchTolerance,
|
|
79
|
+
minStripAreaFraction: options?.minStripAreaFraction ?? DEFAULT_OPTIONS.minStripAreaFraction
|
|
80
|
+
};
|
|
72
81
|
const imageData = ctx.getImageData(0, 0, width, height);
|
|
73
82
|
const { data } = imageData;
|
|
74
83
|
const pixelCount = width * height;
|
|
@@ -114,19 +123,16 @@ var stripEdgeBackground = (ctx, width, height, options) => {
|
|
|
114
123
|
enqueueIfBackground(y * width);
|
|
115
124
|
enqueueIfBackground(y * width + (width - 1));
|
|
116
125
|
}
|
|
126
|
+
const bgAvgR = bgColorSeedCount > 0 ? bgColorSumR / bgColorSeedCount : 0;
|
|
127
|
+
const bgAvgG = bgColorSeedCount > 0 ? bgColorSumG / bgColorSeedCount : 0;
|
|
128
|
+
const bgAvgB = bgColorSeedCount > 0 ? bgColorSumB / bgColorSeedCount : 0;
|
|
129
|
+
const matchesLearnedBackground = (r, g, b) => bgColorSeedCount > 0 && Math.max(Math.abs(r - bgAvgR), Math.abs(g - bgAvgG), Math.abs(b - bgAvgB)) <= opts.interiorBackgroundMatchTolerance;
|
|
117
130
|
if (bgColorSeedCount > 0 && isFlatImage(data, pixelCount, opts.flatImageColorCoverage, opts.flatImageColorQuantLevels)) {
|
|
118
|
-
const bgAvgR = bgColorSumR / bgColorSeedCount;
|
|
119
|
-
const bgAvgG = bgColorSumG / bgColorSeedCount;
|
|
120
|
-
const bgAvgB = bgColorSumB / bgColorSeedCount;
|
|
121
131
|
for (let idx = 0; idx < pixelCount; idx++) {
|
|
122
132
|
if (seeded[idx]) continue;
|
|
123
133
|
const p = idx * 4;
|
|
124
134
|
if ((data[p + 3] ?? 0) === 0) continue;
|
|
125
|
-
|
|
126
|
-
const g = data[p + 1] ?? 0;
|
|
127
|
-
const b = data[p + 2] ?? 0;
|
|
128
|
-
const distance = Math.max(Math.abs(r - bgAvgR), Math.abs(g - bgAvgG), Math.abs(b - bgAvgB));
|
|
129
|
-
if (distance > opts.interiorBackgroundMatchTolerance) continue;
|
|
135
|
+
if (!matchesLearnedBackground(data[p] ?? 0, data[p + 1] ?? 0, data[p + 2] ?? 0)) continue;
|
|
130
136
|
seeded[idx] = 1;
|
|
131
137
|
isBackground[idx] = 1;
|
|
132
138
|
drift[idx] = 0;
|
|
@@ -149,6 +155,7 @@ var stripEdgeBackground = (ctx, width, height, options) => {
|
|
|
149
155
|
if (y > 0) neighbors.push(idx - width);
|
|
150
156
|
if (y < height - 1) neighbors.push(idx + width);
|
|
151
157
|
const currentDrift = drift[idx] ?? 0;
|
|
158
|
+
const fromTransparent = (data[p + 3] ?? 0) === 0;
|
|
152
159
|
for (const nIdx of neighbors) {
|
|
153
160
|
if (seeded[nIdx]) continue;
|
|
154
161
|
const np = nIdx * 4;
|
|
@@ -162,6 +169,14 @@ var stripEdgeBackground = (ctx, width, height, options) => {
|
|
|
162
169
|
const nr = data[np] ?? 0;
|
|
163
170
|
const ng = data[np + 1] ?? 0;
|
|
164
171
|
const nb = data[np + 2] ?? 0;
|
|
172
|
+
if (fromTransparent) {
|
|
173
|
+
if (!isNearNeutral(nr, ng, nb, opts.neutralChannelSpread) || !matchesLearnedBackground(nr, ng, nb)) continue;
|
|
174
|
+
seeded[nIdx] = 1;
|
|
175
|
+
isBackground[nIdx] = 1;
|
|
176
|
+
drift[nIdx] = 0;
|
|
177
|
+
queue.push(nIdx);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
165
180
|
const step = Math.max(Math.abs(nr - r0), Math.abs(ng - g0), Math.abs(nb - b0));
|
|
166
181
|
if (step > opts.floodFillStepTolerance) continue;
|
|
167
182
|
const newDrift = currentDrift + step;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/strip-background.ts","../src/prepare-sprite.ts"],"sourcesContent":["export { stripEdgeBackground } from './strip-background'\nexport type { StripBackgroundOptions } from './strip-background'\nexport { prepareSprite } from './prepare-sprite'\nexport type { PrepareSpriteOptions } from './prepare-sprite'\n","export type StripBackgroundOptions = {\n // Gates which edge pixels can seed the fill (near-black/gray/white only, so colored\n // backgrounds are left alone).\n neutralChannelSpread?: number\n // Bounds how far a pixel's color may drift from the already-filled neighbor that\n // reached it, so a single anti-aliased/noisy step gets absorbed but a hard-edge\n // outline stops the fill cold.\n floodFillStepTolerance?: number\n // Bounds the *total* drift accumulated over the whole chain of hops back to the\n // border seed — without it, many small sub-tolerance steps can chain together and\n // worm past a real outline into an interior region that happens to share a similar\n // tone.\n floodFillMaxDrift?: number\n // How many background pixels near the stopping edge get a soft alpha ramp instead\n // of a hard 0/255 cutoff.\n falloffRadiusPx?: number\n // Fraction of opaque pixels the two most common color buckets must cover for an\n // image to be treated as flat/bilevel line-art (only then is it safe to also seed\n // interior background holes — see isFlatImage).\n flatImageColorCoverage?: number\n // How finely colors are bucketed when checking flat-image coverage.\n flatImageColorQuantLevels?: number\n // How close an interior pixel must be to the *learned* border-background color (not\n // just \"near-neutral\") to be seeded directly on a flat image, so a flat image's\n // foreground color (also near-neutral, e.g. black ink) isn't swept up just for\n // being grayscale.\n interiorBackgroundMatchTolerance?: number\n // Below this fraction of the original opaque content, an entire strip result is\n // abandoned and the image is left untouched — guards against textured photo\n // backgrounds (a brick wall, grout lines) nibbling a small, irregular bite out of\n // one edge instead of either clearing the background cleanly or matching nothing.\n minStripAreaFraction?: number\n}\n\nconst DEFAULT_OPTIONS: Required<StripBackgroundOptions> = {\n neutralChannelSpread: 20,\n floodFillStepTolerance: 10,\n floodFillMaxDrift: 24,\n falloffRadiusPx: 3,\n flatImageColorCoverage: 0.92,\n flatImageColorQuantLevels: 32,\n interiorBackgroundMatchTolerance: 16,\n minStripAreaFraction: 0.03,\n}\n\nconst isNearNeutral = (r: number, g: number, b: number, spread: number): boolean => {\n const max = Math.max(r, g, b)\n const min = Math.min(r, g, b)\n return max - min <= spread\n}\n\n// Quantizes every opaque pixel's color and checks whether the two most common buckets\n// cover nearly the whole image — true for bilevel line art/logos/signatures, false for\n// photos and other continuous-tone images where a same-colored subject region could\n// plausibly exist.\nconst isFlatImage = (\n data: Uint8ClampedArray,\n pixelCount: number,\n coverage: number,\n quantLevels: number,\n): boolean => {\n const bucketCounts = new Map<number, number>()\n let opaqueCount = 0\n for (let idx = 0; idx < pixelCount; idx++) {\n const p = idx * 4\n if ((data[p + 3] ?? 0) === 0) continue\n opaqueCount += 1\n const rBucket = Math.floor(((data[p] ?? 0) / 256) * quantLevels)\n const gBucket = Math.floor(((data[p + 1] ?? 0) / 256) * quantLevels)\n const bBucket = Math.floor(((data[p + 2] ?? 0) / 256) * quantLevels)\n const key = (rBucket * quantLevels + gBucket) * quantLevels + bBucket\n bucketCounts.set(key, (bucketCounts.get(key) ?? 0) + 1)\n }\n if (opaqueCount === 0) return false\n let top1 = 0\n let top2 = 0\n for (const count of bucketCounts.values()) {\n if (count > top1) {\n top2 = top1\n top1 = count\n } else if (count > top2) {\n top2 = count\n }\n }\n return (top1 + top2) / opaqueCount >= coverage\n}\n\n// Flood-fills inward from the canvas edges, removing only pixels reachable from a\n// near-neutral border through a chain of locally-similar neighbors, then feathers the\n// resulting cut edge with a short alpha falloff instead of leaving a hard binary mask.\nexport const stripEdgeBackground = (\n ctx: CanvasRenderingContext2D,\n width: number,\n height: number,\n options?: StripBackgroundOptions,\n): void => {\n const opts = { ...DEFAULT_OPTIONS, ...options }\n const imageData = ctx.getImageData(0, 0, width, height)\n const { data } = imageData\n const pixelCount = width * height\n const isBackground = new Uint8Array(pixelCount)\n const seeded = new Uint8Array(pixelCount)\n const drift = new Uint16Array(pixelCount)\n const queue: number[] = []\n\n // Rectangular uploads may arrive already sitting on transparent square padding —\n // that padding is deliberate and shouldn't count as \"background removed\" one way or\n // the other, so the area-fraction sanity check below is measured only against pixels\n // that started out opaque (the real content), not the full padded canvas.\n const wasOriginallyOpaque = new Uint8Array(pixelCount)\n let originalOpaqueCount = 0\n for (let idx = 0; idx < pixelCount; idx++) {\n if ((data[idx * 4 + 3] ?? 0) === 0) continue\n wasOriginallyOpaque[idx] = 1\n originalOpaqueCount += 1\n }\n\n let bgColorSumR = 0\n let bgColorSumG = 0\n let bgColorSumB = 0\n let bgColorSeedCount = 0\n\n const enqueueIfBackground = (idx: number): void => {\n if (seeded[idx]) return\n const p = idx * 4\n const r = data[p] ?? 0\n const g = data[p + 1] ?? 0\n const b = data[p + 2] ?? 0\n const isTransparent = (data[p + 3] ?? 0) === 0\n if (!isTransparent && !isNearNeutral(r, g, b, opts.neutralChannelSpread)) return\n seeded[idx] = 1\n isBackground[idx] = 1\n drift[idx] = 0\n if (!isTransparent) {\n bgColorSumR += r\n bgColorSumG += g\n bgColorSumB += b\n bgColorSeedCount += 1\n }\n queue.push(idx)\n }\n\n for (let x = 0; x < width; x++) {\n enqueueIfBackground(x)\n enqueueIfBackground((height - 1) * width + x)\n }\n for (let y = 0; y < height; y++) {\n enqueueIfBackground(y * width)\n enqueueIfBackground(y * width + (width - 1))\n }\n\n // Flat/line-art images (few distinct colors, so no same-toned subject can plausibly\n // exist) also get interior pixels seeded directly whenever they closely match the\n // learned border-background color — this is what lets a fully-enclosed hole (e.g.\n // the loop of a cursive letter) get removed even though it never touches the canvas\n // edge. Photographic images skip this and keep the conservative border-only\n // behavior, since color alone can't tell a hole from a same-colored subject region\n // (an eye, a shirt) once ML-level semantics are needed.\n if (bgColorSeedCount > 0 && isFlatImage(data, pixelCount, opts.flatImageColorCoverage, opts.flatImageColorQuantLevels)) {\n const bgAvgR = bgColorSumR / bgColorSeedCount\n const bgAvgG = bgColorSumG / bgColorSeedCount\n const bgAvgB = bgColorSumB / bgColorSeedCount\n for (let idx = 0; idx < pixelCount; idx++) {\n if (seeded[idx]) continue\n const p = idx * 4\n if ((data[p + 3] ?? 0) === 0) continue\n const r = data[p] ?? 0\n const g = data[p + 1] ?? 0\n const b = data[p + 2] ?? 0\n const distance = Math.max(Math.abs(r - bgAvgR), Math.abs(g - bgAvgG), Math.abs(b - bgAvgB))\n if (distance > opts.interiorBackgroundMatchTolerance) continue\n seeded[idx] = 1\n isBackground[idx] = 1\n drift[idx] = 0\n queue.push(idx)\n }\n }\n\n let head = 0\n while (head < queue.length) {\n const idx = queue[head] ?? -1\n head += 1\n const x = idx % width\n const y = Math.floor(idx / width)\n const p = idx * 4\n const r0 = data[p] ?? 0\n const g0 = data[p + 1] ?? 0\n const b0 = data[p + 2] ?? 0\n\n const neighbors: number[] = []\n if (x > 0) neighbors.push(idx - 1)\n if (x < width - 1) neighbors.push(idx + 1)\n if (y > 0) neighbors.push(idx - width)\n if (y < height - 1) neighbors.push(idx + width)\n\n const currentDrift = drift[idx] ?? 0\n\n for (const nIdx of neighbors) {\n if (seeded[nIdx]) continue\n const np = nIdx * 4\n if ((data[np + 3] ?? 0) === 0) {\n seeded[nIdx] = 1\n isBackground[nIdx] = 1\n drift[nIdx] = currentDrift\n queue.push(nIdx)\n continue\n }\n const nr = data[np] ?? 0\n const ng = data[np + 1] ?? 0\n const nb = data[np + 2] ?? 0\n const step = Math.max(Math.abs(nr - r0), Math.abs(ng - g0), Math.abs(nb - b0))\n if (step > opts.floodFillStepTolerance) continue\n const newDrift = currentDrift + step\n if (newDrift > opts.floodFillMaxDrift) continue\n seeded[nIdx] = 1\n isBackground[nIdx] = 1\n drift[nIdx] = newDrift\n queue.push(nIdx)\n }\n }\n\n let strippedContentCount = 0\n for (let idx = 0; idx < pixelCount; idx++) {\n if (isBackground[idx] && wasOriginallyOpaque[idx]) strippedContentCount += 1\n }\n if (originalOpaqueCount === 0 || strippedContentCount / originalOpaqueCount < opts.minStripAreaFraction) return\n\n const distanceFromForeground = new Int16Array(pixelCount).fill(-1)\n const falloffQueue: number[] = []\n for (let idx = 0; idx < pixelCount; idx++) {\n if (!isBackground[idx]) continue\n const x = idx % width\n const y = Math.floor(idx / width)\n const touchesForeground =\n (x > 0 && !isBackground[idx - 1]) ||\n (x < width - 1 && !isBackground[idx + 1]) ||\n (y > 0 && !isBackground[idx - width]) ||\n (y < height - 1 && !isBackground[idx + width])\n if (touchesForeground) {\n distanceFromForeground[idx] = 0\n falloffQueue.push(idx)\n }\n }\n let fHead = 0\n while (fHead < falloffQueue.length) {\n const idx = falloffQueue[fHead] ?? -1\n fHead += 1\n const d = distanceFromForeground[idx] ?? -1\n if (d >= opts.falloffRadiusPx - 1) continue\n const x = idx % width\n const y = Math.floor(idx / width)\n const neighbors: number[] = []\n if (x > 0) neighbors.push(idx - 1)\n if (x < width - 1) neighbors.push(idx + 1)\n if (y > 0) neighbors.push(idx - width)\n if (y < height - 1) neighbors.push(idx + width)\n for (const nIdx of neighbors) {\n if (!isBackground[nIdx]) continue\n if ((distanceFromForeground[nIdx] ?? -1) !== -1) continue\n distanceFromForeground[nIdx] = d + 1\n falloffQueue.push(nIdx)\n }\n }\n\n for (let idx = 0; idx < pixelCount; idx++) {\n if (!isBackground[idx]) continue\n const p = idx * 4\n const d = distanceFromForeground[idx] ?? -1\n const origAlpha = data[p + 3] ?? 0\n data[p + 3] = d === -1 ? 0 : Math.round((origAlpha * (opts.falloffRadiusPx - d)) / (opts.falloffRadiusPx + 1))\n }\n\n ctx.putImageData(imageData, 0, 0)\n}\n","import { stripEdgeBackground, StripBackgroundOptions } from './strip-background'\n\nexport type PrepareSpriteOptions = StripBackgroundOptions & {\n // Set false to only resize/square-pad and skip the background strip step.\n stripBackground?: boolean\n}\n\n// Decodes a data URL, downscales it to fit maxSide if needed, and returns a new PNG\n// data URL. The particle/point-sprite renderers this was built for always billboard a\n// sprite as a square, so rectangular input is centered on a square transparent-padded\n// canvas rather than stretched — the padding absorbs the square billboard, and the\n// content keeps its original proportions. Background stripping (see stripEdgeBackground)\n// runs on the result unless disabled via options.\nexport const prepareSprite = (dataUrl: string, maxSide: number, options?: PrepareSpriteOptions): Promise<string> => {\n return new Promise((resolve, reject) => {\n const img = new Image()\n img.onload = (): void => {\n const w = img.naturalWidth\n const h = img.naturalHeight\n if (w === 0 || h === 0) {\n resolve(dataUrl)\n return\n }\n const maxDim = Math.max(w, h)\n const scale = maxDim > maxSide ? maxSide / maxDim : 1\n const newW = Math.max(1, Math.round(w * scale))\n const newH = Math.max(1, Math.round(h * scale))\n const side = Math.max(newW, newH)\n const offsetX = Math.floor((side - newW) / 2)\n const offsetY = Math.floor((side - newH) / 2)\n const canvas = document.createElement('canvas')\n canvas.width = side\n canvas.height = side\n const ctx = canvas.getContext('2d', { willReadFrequently: true })\n if (!ctx) {\n resolve(dataUrl)\n return\n }\n ctx.imageSmoothingEnabled = true\n ctx.imageSmoothingQuality = 'high'\n ctx.drawImage(img, offsetX, offsetY, newW, newH)\n if (options?.stripBackground !== false) {\n stripEdgeBackground(ctx, side, side, options)\n }\n resolve(canvas.toDataURL('image/png'))\n }\n img.onerror = (): void => reject(new Error('decode'))\n img.src = dataUrl\n })\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/strip-background.ts","../src/prepare-sprite.ts"],"sourcesContent":["export { stripEdgeBackground } from './strip-background'\nexport type { StripBackgroundOptions } from './strip-background'\nexport { prepareSprite } from './prepare-sprite'\nexport type { PrepareSpriteOptions } from './prepare-sprite'\n","export type StripBackgroundOptions = {\n // Gates which edge pixels can seed the fill (near-black/gray/white only, so colored\n // backgrounds are left alone).\n neutralChannelSpread?: number\n // Bounds how far a pixel's color may drift from the already-filled neighbor that\n // reached it, so a single anti-aliased/noisy step gets absorbed but a hard-edge\n // outline stops the fill cold.\n floodFillStepTolerance?: number\n // Bounds the *total* drift accumulated over the whole chain of hops back to the\n // border seed — without it, many small sub-tolerance steps can chain together and\n // worm past a real outline into an interior region that happens to share a similar\n // tone.\n floodFillMaxDrift?: number\n // How many background pixels near the stopping edge get a soft alpha ramp instead\n // of a hard 0/255 cutoff.\n falloffRadiusPx?: number\n // Fraction of opaque pixels the two most common color buckets must cover for an\n // image to be treated as flat/bilevel line-art (only then is it safe to also seed\n // interior background holes — see isFlatImage).\n flatImageColorCoverage?: number\n // How finely colors are bucketed when checking flat-image coverage.\n flatImageColorQuantLevels?: number\n // How close an interior pixel must be to the *learned* border-background color (not\n // just \"near-neutral\") to be seeded directly on a flat image, so a flat image's\n // foreground color (also near-neutral, e.g. black ink) isn't swept up just for\n // being grayscale.\n interiorBackgroundMatchTolerance?: number\n // Below this fraction of the original opaque content, an entire strip result is\n // abandoned and the image is left untouched — guards against textured photo\n // backgrounds (a brick wall, grout lines) nibbling a small, irregular bite out of\n // one edge instead of either clearing the background cleanly or matching nothing.\n minStripAreaFraction?: number\n}\n\nconst DEFAULT_OPTIONS: Required<StripBackgroundOptions> = {\n neutralChannelSpread: 20,\n floodFillStepTolerance: 10,\n floodFillMaxDrift: 24,\n falloffRadiusPx: 3,\n flatImageColorCoverage: 0.92,\n flatImageColorQuantLevels: 32,\n interiorBackgroundMatchTolerance: 16,\n minStripAreaFraction: 0.03,\n}\n\nconst isNearNeutral = (r: number, g: number, b: number, spread: number): boolean => {\n const max = Math.max(r, g, b)\n const min = Math.min(r, g, b)\n return max - min <= spread\n}\n\n// Quantizes every opaque pixel's color and checks whether the two most common buckets\n// cover nearly the whole image — true for bilevel line art/logos/signatures, false for\n// photos and other continuous-tone images where a same-colored subject region could\n// plausibly exist.\nconst isFlatImage = (\n data: Uint8ClampedArray,\n pixelCount: number,\n coverage: number,\n quantLevels: number,\n): boolean => {\n const bucketCounts = new Map<number, number>()\n let opaqueCount = 0\n for (let idx = 0; idx < pixelCount; idx++) {\n const p = idx * 4\n if ((data[p + 3] ?? 0) === 0) continue\n opaqueCount += 1\n const rBucket = Math.floor(((data[p] ?? 0) / 256) * quantLevels)\n const gBucket = Math.floor(((data[p + 1] ?? 0) / 256) * quantLevels)\n const bBucket = Math.floor(((data[p + 2] ?? 0) / 256) * quantLevels)\n const key = (rBucket * quantLevels + gBucket) * quantLevels + bBucket\n bucketCounts.set(key, (bucketCounts.get(key) ?? 0) + 1)\n }\n if (opaqueCount === 0) return false\n let top1 = 0\n let top2 = 0\n for (const count of bucketCounts.values()) {\n if (count > top1) {\n top2 = top1\n top1 = count\n } else if (count > top2) {\n top2 = count\n }\n }\n return (top1 + top2) / opaqueCount >= coverage\n}\n\n// Flood-fills inward from the canvas edges, removing only pixels reachable from a\n// near-neutral border through a chain of locally-similar neighbors, then feathers the\n// resulting cut edge with a short alpha falloff instead of leaving a hard binary mask.\nexport const stripEdgeBackground = (\n ctx: CanvasRenderingContext2D,\n width: number,\n height: number,\n options?: StripBackgroundOptions,\n): void => {\n // Per field, so an option passed as `undefined` keeps its default instead of replacing it.\n const opts: Required<StripBackgroundOptions> = {\n neutralChannelSpread: options?.neutralChannelSpread ?? DEFAULT_OPTIONS.neutralChannelSpread,\n floodFillStepTolerance: options?.floodFillStepTolerance ?? DEFAULT_OPTIONS.floodFillStepTolerance,\n floodFillMaxDrift: options?.floodFillMaxDrift ?? DEFAULT_OPTIONS.floodFillMaxDrift,\n falloffRadiusPx: options?.falloffRadiusPx ?? DEFAULT_OPTIONS.falloffRadiusPx,\n flatImageColorCoverage: options?.flatImageColorCoverage ?? DEFAULT_OPTIONS.flatImageColorCoverage,\n flatImageColorQuantLevels: options?.flatImageColorQuantLevels ?? DEFAULT_OPTIONS.flatImageColorQuantLevels,\n interiorBackgroundMatchTolerance: options?.interiorBackgroundMatchTolerance ?? DEFAULT_OPTIONS.interiorBackgroundMatchTolerance,\n minStripAreaFraction: options?.minStripAreaFraction ?? DEFAULT_OPTIONS.minStripAreaFraction,\n }\n const imageData = ctx.getImageData(0, 0, width, height)\n const { data } = imageData\n const pixelCount = width * height\n const isBackground = new Uint8Array(pixelCount)\n const seeded = new Uint8Array(pixelCount)\n const drift = new Uint16Array(pixelCount)\n const queue: number[] = []\n\n // Rectangular uploads may arrive already sitting on transparent square padding —\n // that padding is deliberate and shouldn't count as \"background removed\" one way or\n // the other, so the area-fraction sanity check below is measured only against pixels\n // that started out opaque (the real content), not the full padded canvas.\n const wasOriginallyOpaque = new Uint8Array(pixelCount)\n let originalOpaqueCount = 0\n for (let idx = 0; idx < pixelCount; idx++) {\n if ((data[idx * 4 + 3] ?? 0) === 0) continue\n wasOriginallyOpaque[idx] = 1\n originalOpaqueCount += 1\n }\n\n let bgColorSumR = 0\n let bgColorSumG = 0\n let bgColorSumB = 0\n let bgColorSeedCount = 0\n\n const enqueueIfBackground = (idx: number): void => {\n if (seeded[idx]) return\n const p = idx * 4\n const r = data[p] ?? 0\n const g = data[p + 1] ?? 0\n const b = data[p + 2] ?? 0\n const isTransparent = (data[p + 3] ?? 0) === 0\n if (!isTransparent && !isNearNeutral(r, g, b, opts.neutralChannelSpread)) return\n seeded[idx] = 1\n isBackground[idx] = 1\n drift[idx] = 0\n if (!isTransparent) {\n bgColorSumR += r\n bgColorSumG += g\n bgColorSumB += b\n bgColorSeedCount += 1\n }\n queue.push(idx)\n }\n\n for (let x = 0; x < width; x++) {\n enqueueIfBackground(x)\n enqueueIfBackground((height - 1) * width + x)\n }\n for (let y = 0; y < height; y++) {\n enqueueIfBackground(y * width)\n enqueueIfBackground(y * width + (width - 1))\n }\n\n // Flat/line-art images (few distinct colors, so no same-toned subject can plausibly\n // exist) also get interior pixels seeded directly whenever they closely match the\n // learned border-background color — this is what lets a fully-enclosed hole (e.g.\n // the loop of a cursive letter) get removed even though it never touches the canvas\n // edge. Photographic images skip this and keep the conservative border-only\n // behavior, since color alone can't tell a hole from a same-colored subject region\n // (an eye, a shirt) once ML-level semantics are needed.\n const bgAvgR = bgColorSeedCount > 0 ? bgColorSumR / bgColorSeedCount : 0\n const bgAvgG = bgColorSeedCount > 0 ? bgColorSumG / bgColorSeedCount : 0\n const bgAvgB = bgColorSeedCount > 0 ? bgColorSumB / bgColorSeedCount : 0\n // Close to the background color learned from the opaque border pixels (never true when the border was all transparent).\n const matchesLearnedBackground = (r: number, g: number, b: number): boolean =>\n bgColorSeedCount > 0 &&\n Math.max(Math.abs(r - bgAvgR), Math.abs(g - bgAvgG), Math.abs(b - bgAvgB)) <= opts.interiorBackgroundMatchTolerance\n\n if (bgColorSeedCount > 0 && isFlatImage(data, pixelCount, opts.flatImageColorCoverage, opts.flatImageColorQuantLevels)) {\n for (let idx = 0; idx < pixelCount; idx++) {\n if (seeded[idx]) continue\n const p = idx * 4\n if ((data[p + 3] ?? 0) === 0) continue\n if (!matchesLearnedBackground(data[p] ?? 0, data[p + 1] ?? 0, data[p + 2] ?? 0)) continue\n seeded[idx] = 1\n isBackground[idx] = 1\n drift[idx] = 0\n queue.push(idx)\n }\n }\n\n let head = 0\n while (head < queue.length) {\n const idx = queue[head] ?? -1\n head += 1\n const x = idx % width\n const y = Math.floor(idx / width)\n const p = idx * 4\n const r0 = data[p] ?? 0\n const g0 = data[p + 1] ?? 0\n const b0 = data[p + 2] ?? 0\n\n const neighbors: number[] = []\n if (x > 0) neighbors.push(idx - 1)\n if (x < width - 1) neighbors.push(idx + 1)\n if (y > 0) neighbors.push(idx - width)\n if (y < height - 1) neighbors.push(idx + width)\n\n const currentDrift = drift[idx] ?? 0\n // A transparent pixel's RGB means nothing (usually 0,0,0), so stepping from padding into content is not a color step: it takes an\n // opaque pixel only when it reads as the background itself, the way a border seed would, and the drift starts over there.\n const fromTransparent = (data[p + 3] ?? 0) === 0\n\n for (const nIdx of neighbors) {\n if (seeded[nIdx]) continue\n const np = nIdx * 4\n if ((data[np + 3] ?? 0) === 0) {\n seeded[nIdx] = 1\n isBackground[nIdx] = 1\n drift[nIdx] = currentDrift\n queue.push(nIdx)\n continue\n }\n const nr = data[np] ?? 0\n const ng = data[np + 1] ?? 0\n const nb = data[np + 2] ?? 0\n if (fromTransparent) {\n if (!isNearNeutral(nr, ng, nb, opts.neutralChannelSpread) || !matchesLearnedBackground(nr, ng, nb)) continue\n seeded[nIdx] = 1\n isBackground[nIdx] = 1\n drift[nIdx] = 0\n queue.push(nIdx)\n continue\n }\n const step = Math.max(Math.abs(nr - r0), Math.abs(ng - g0), Math.abs(nb - b0))\n if (step > opts.floodFillStepTolerance) continue\n const newDrift = currentDrift + step\n if (newDrift > opts.floodFillMaxDrift) continue\n seeded[nIdx] = 1\n isBackground[nIdx] = 1\n drift[nIdx] = newDrift\n queue.push(nIdx)\n }\n }\n\n let strippedContentCount = 0\n for (let idx = 0; idx < pixelCount; idx++) {\n if (isBackground[idx] && wasOriginallyOpaque[idx]) strippedContentCount += 1\n }\n if (originalOpaqueCount === 0 || strippedContentCount / originalOpaqueCount < opts.minStripAreaFraction) return\n\n const distanceFromForeground = new Int16Array(pixelCount).fill(-1)\n const falloffQueue: number[] = []\n for (let idx = 0; idx < pixelCount; idx++) {\n if (!isBackground[idx]) continue\n const x = idx % width\n const y = Math.floor(idx / width)\n const touchesForeground =\n (x > 0 && !isBackground[idx - 1]) ||\n (x < width - 1 && !isBackground[idx + 1]) ||\n (y > 0 && !isBackground[idx - width]) ||\n (y < height - 1 && !isBackground[idx + width])\n if (touchesForeground) {\n distanceFromForeground[idx] = 0\n falloffQueue.push(idx)\n }\n }\n let fHead = 0\n while (fHead < falloffQueue.length) {\n const idx = falloffQueue[fHead] ?? -1\n fHead += 1\n const d = distanceFromForeground[idx] ?? -1\n if (d >= opts.falloffRadiusPx - 1) continue\n const x = idx % width\n const y = Math.floor(idx / width)\n const neighbors: number[] = []\n if (x > 0) neighbors.push(idx - 1)\n if (x < width - 1) neighbors.push(idx + 1)\n if (y > 0) neighbors.push(idx - width)\n if (y < height - 1) neighbors.push(idx + width)\n for (const nIdx of neighbors) {\n if (!isBackground[nIdx]) continue\n if ((distanceFromForeground[nIdx] ?? -1) !== -1) continue\n distanceFromForeground[nIdx] = d + 1\n falloffQueue.push(nIdx)\n }\n }\n\n for (let idx = 0; idx < pixelCount; idx++) {\n if (!isBackground[idx]) continue\n const p = idx * 4\n const d = distanceFromForeground[idx] ?? -1\n const origAlpha = data[p + 3] ?? 0\n data[p + 3] = d === -1 ? 0 : Math.round((origAlpha * (opts.falloffRadiusPx - d)) / (opts.falloffRadiusPx + 1))\n }\n\n ctx.putImageData(imageData, 0, 0)\n}\n","import { stripEdgeBackground, StripBackgroundOptions } from './strip-background'\n\nexport type PrepareSpriteOptions = StripBackgroundOptions & {\n // Set false to only resize/square-pad and skip the background strip step.\n stripBackground?: boolean\n}\n\n// Decodes a data URL, downscales it to fit maxSide if needed, and returns a new PNG\n// data URL. The particle/point-sprite renderers this was built for always billboard a\n// sprite as a square, so rectangular input is centered on a square transparent-padded\n// canvas rather than stretched — the padding absorbs the square billboard, and the\n// content keeps its original proportions. Background stripping (see stripEdgeBackground)\n// runs on the result unless disabled via options.\nexport const prepareSprite = (dataUrl: string, maxSide: number, options?: PrepareSpriteOptions): Promise<string> => {\n return new Promise((resolve, reject) => {\n const img = new Image()\n img.onload = (): void => {\n const w = img.naturalWidth\n const h = img.naturalHeight\n if (w === 0 || h === 0) {\n resolve(dataUrl)\n return\n }\n const maxDim = Math.max(w, h)\n const scale = maxDim > maxSide ? maxSide / maxDim : 1\n const newW = Math.max(1, Math.round(w * scale))\n const newH = Math.max(1, Math.round(h * scale))\n const side = Math.max(newW, newH)\n const offsetX = Math.floor((side - newW) / 2)\n const offsetY = Math.floor((side - newH) / 2)\n const canvas = document.createElement('canvas')\n canvas.width = side\n canvas.height = side\n const ctx = canvas.getContext('2d', { willReadFrequently: true })\n if (!ctx) {\n resolve(dataUrl)\n return\n }\n ctx.imageSmoothingEnabled = true\n ctx.imageSmoothingQuality = 'high'\n ctx.drawImage(img, offsetX, offsetY, newW, newH)\n if (options?.stripBackground !== false) {\n stripEdgeBackground(ctx, side, side, options)\n }\n resolve(canvas.toDataURL('image/png'))\n }\n img.onerror = (): void => reject(new Error('decode'))\n img.src = dataUrl\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACkCA,IAAM,kBAAoD;AAAA,EACxD,sBAAsB;AAAA,EACtB,wBAAwB;AAAA,EACxB,mBAAmB;AAAA,EACnB,iBAAiB;AAAA,EACjB,wBAAwB;AAAA,EACxB,2BAA2B;AAAA,EAC3B,kCAAkC;AAAA,EAClC,sBAAsB;AACxB;AAEA,IAAM,gBAAgB,CAAC,GAAW,GAAW,GAAW,WAA4B;AAClF,QAAM,MAAM,KAAK,IAAI,GAAG,GAAG,CAAC;AAC5B,QAAM,MAAM,KAAK,IAAI,GAAG,GAAG,CAAC;AAC5B,SAAO,MAAM,OAAO;AACtB;AAMA,IAAM,cAAc,CAClB,MACA,YACA,UACA,gBACY;AACZ,QAAM,eAAe,oBAAI,IAAoB;AAC7C,MAAI,cAAc;AAClB,WAAS,MAAM,GAAG,MAAM,YAAY,OAAO;AACzC,UAAM,IAAI,MAAM;AAChB,SAAK,KAAK,IAAI,CAAC,KAAK,OAAO,EAAG;AAC9B,mBAAe;AACf,UAAM,UAAU,KAAK,OAAQ,KAAK,CAAC,KAAK,KAAK,MAAO,WAAW;AAC/D,UAAM,UAAU,KAAK,OAAQ,KAAK,IAAI,CAAC,KAAK,KAAK,MAAO,WAAW;AACnE,UAAM,UAAU,KAAK,OAAQ,KAAK,IAAI,CAAC,KAAK,KAAK,MAAO,WAAW;AACnE,UAAM,OAAO,UAAU,cAAc,WAAW,cAAc;AAC9D,iBAAa,IAAI,MAAM,aAAa,IAAI,GAAG,KAAK,KAAK,CAAC;AAAA,EACxD;AACA,MAAI,gBAAgB,EAAG,QAAO;AAC9B,MAAI,OAAO;AACX,MAAI,OAAO;AACX,aAAW,SAAS,aAAa,OAAO,GAAG;AACzC,QAAI,QAAQ,MAAM;AAChB,aAAO;AACP,aAAO;AAAA,IACT,WAAW,QAAQ,MAAM;AACvB,aAAO;AAAA,IACT;AAAA,EACF;AACA,UAAQ,OAAO,QAAQ,eAAe;AACxC;AAKO,IAAM,sBAAsB,CACjC,KACA,OACA,QACA,YACS;AAET,QAAM,OAAyC;AAAA,IAC7C,sBAAsB,SAAS,wBAAwB,gBAAgB;AAAA,IACvE,wBAAwB,SAAS,0BAA0B,gBAAgB;AAAA,IAC3E,mBAAmB,SAAS,qBAAqB,gBAAgB;AAAA,IACjE,iBAAiB,SAAS,mBAAmB,gBAAgB;AAAA,IAC7D,wBAAwB,SAAS,0BAA0B,gBAAgB;AAAA,IAC3E,2BAA2B,SAAS,6BAA6B,gBAAgB;AAAA,IACjF,kCAAkC,SAAS,oCAAoC,gBAAgB;AAAA,IAC/F,sBAAsB,SAAS,wBAAwB,gBAAgB;AAAA,EACzE;AACA,QAAM,YAAY,IAAI,aAAa,GAAG,GAAG,OAAO,MAAM;AACtD,QAAM,EAAE,KAAK,IAAI;AACjB,QAAM,aAAa,QAAQ;AAC3B,QAAM,eAAe,IAAI,WAAW,UAAU;AAC9C,QAAM,SAAS,IAAI,WAAW,UAAU;AACxC,QAAM,QAAQ,IAAI,YAAY,UAAU;AACxC,QAAM,QAAkB,CAAC;AAMzB,QAAM,sBAAsB,IAAI,WAAW,UAAU;AACrD,MAAI,sBAAsB;AAC1B,WAAS,MAAM,GAAG,MAAM,YAAY,OAAO;AACzC,SAAK,KAAK,MAAM,IAAI,CAAC,KAAK,OAAO,EAAG;AACpC,wBAAoB,GAAG,IAAI;AAC3B,2BAAuB;AAAA,EACzB;AAEA,MAAI,cAAc;AAClB,MAAI,cAAc;AAClB,MAAI,cAAc;AAClB,MAAI,mBAAmB;AAEvB,QAAM,sBAAsB,CAAC,QAAsB;AACjD,QAAI,OAAO,GAAG,EAAG;AACjB,UAAM,IAAI,MAAM;AAChB,UAAM,IAAI,KAAK,CAAC,KAAK;AACrB,UAAM,IAAI,KAAK,IAAI,CAAC,KAAK;AACzB,UAAM,IAAI,KAAK,IAAI,CAAC,KAAK;AACzB,UAAM,iBAAiB,KAAK,IAAI,CAAC,KAAK,OAAO;AAC7C,QAAI,CAAC,iBAAiB,CAAC,cAAc,GAAG,GAAG,GAAG,KAAK,oBAAoB,EAAG;AAC1E,WAAO,GAAG,IAAI;AACd,iBAAa,GAAG,IAAI;AACpB,UAAM,GAAG,IAAI;AACb,QAAI,CAAC,eAAe;AAClB,qBAAe;AACf,qBAAe;AACf,qBAAe;AACf,0BAAoB;AAAA,IACtB;AACA,UAAM,KAAK,GAAG;AAAA,EAChB;AAEA,WAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC9B,wBAAoB,CAAC;AACrB,yBAAqB,SAAS,KAAK,QAAQ,CAAC;AAAA,EAC9C;AACA,WAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAC/B,wBAAoB,IAAI,KAAK;AAC7B,wBAAoB,IAAI,SAAS,QAAQ,EAAE;AAAA,EAC7C;AASA,QAAM,SAAS,mBAAmB,IAAI,cAAc,mBAAmB;AACvE,QAAM,SAAS,mBAAmB,IAAI,cAAc,mBAAmB;AACvE,QAAM,SAAS,mBAAmB,IAAI,cAAc,mBAAmB;AAEvE,QAAM,2BAA2B,CAAC,GAAW,GAAW,MACtD,mBAAmB,KACnB,KAAK,IAAI,KAAK,IAAI,IAAI,MAAM,GAAG,KAAK,IAAI,IAAI,MAAM,GAAG,KAAK,IAAI,IAAI,MAAM,CAAC,KAAK,KAAK;AAErF,MAAI,mBAAmB,KAAK,YAAY,MAAM,YAAY,KAAK,wBAAwB,KAAK,yBAAyB,GAAG;AACtH,aAAS,MAAM,GAAG,MAAM,YAAY,OAAO;AACzC,UAAI,OAAO,GAAG,EAAG;AACjB,YAAM,IAAI,MAAM;AAChB,WAAK,KAAK,IAAI,CAAC,KAAK,OAAO,EAAG;AAC9B,UAAI,CAAC,yBAAyB,KAAK,CAAC,KAAK,GAAG,KAAK,IAAI,CAAC,KAAK,GAAG,KAAK,IAAI,CAAC,KAAK,CAAC,EAAG;AACjF,aAAO,GAAG,IAAI;AACd,mBAAa,GAAG,IAAI;AACpB,YAAM,GAAG,IAAI;AACb,YAAM,KAAK,GAAG;AAAA,IAChB;AAAA,EACF;AAEA,MAAI,OAAO;AACX,SAAO,OAAO,MAAM,QAAQ;AAC1B,UAAM,MAAM,MAAM,IAAI,KAAK;AAC3B,YAAQ;AACR,UAAM,IAAI,MAAM;AAChB,UAAM,IAAI,KAAK,MAAM,MAAM,KAAK;AAChC,UAAM,IAAI,MAAM;AAChB,UAAM,KAAK,KAAK,CAAC,KAAK;AACtB,UAAM,KAAK,KAAK,IAAI,CAAC,KAAK;AAC1B,UAAM,KAAK,KAAK,IAAI,CAAC,KAAK;AAE1B,UAAM,YAAsB,CAAC;AAC7B,QAAI,IAAI,EAAG,WAAU,KAAK,MAAM,CAAC;AACjC,QAAI,IAAI,QAAQ,EAAG,WAAU,KAAK,MAAM,CAAC;AACzC,QAAI,IAAI,EAAG,WAAU,KAAK,MAAM,KAAK;AACrC,QAAI,IAAI,SAAS,EAAG,WAAU,KAAK,MAAM,KAAK;AAE9C,UAAM,eAAe,MAAM,GAAG,KAAK;AAGnC,UAAM,mBAAmB,KAAK,IAAI,CAAC,KAAK,OAAO;AAE/C,eAAW,QAAQ,WAAW;AAC5B,UAAI,OAAO,IAAI,EAAG;AAClB,YAAM,KAAK,OAAO;AAClB,WAAK,KAAK,KAAK,CAAC,KAAK,OAAO,GAAG;AAC7B,eAAO,IAAI,IAAI;AACf,qBAAa,IAAI,IAAI;AACrB,cAAM,IAAI,IAAI;AACd,cAAM,KAAK,IAAI;AACf;AAAA,MACF;AACA,YAAM,KAAK,KAAK,EAAE,KAAK;AACvB,YAAM,KAAK,KAAK,KAAK,CAAC,KAAK;AAC3B,YAAM,KAAK,KAAK,KAAK,CAAC,KAAK;AAC3B,UAAI,iBAAiB;AACnB,YAAI,CAAC,cAAc,IAAI,IAAI,IAAI,KAAK,oBAAoB,KAAK,CAAC,yBAAyB,IAAI,IAAI,EAAE,EAAG;AACpG,eAAO,IAAI,IAAI;AACf,qBAAa,IAAI,IAAI;AACrB,cAAM,IAAI,IAAI;AACd,cAAM,KAAK,IAAI;AACf;AAAA,MACF;AACA,YAAM,OAAO,KAAK,IAAI,KAAK,IAAI,KAAK,EAAE,GAAG,KAAK,IAAI,KAAK,EAAE,GAAG,KAAK,IAAI,KAAK,EAAE,CAAC;AAC7E,UAAI,OAAO,KAAK,uBAAwB;AACxC,YAAM,WAAW,eAAe;AAChC,UAAI,WAAW,KAAK,kBAAmB;AACvC,aAAO,IAAI,IAAI;AACf,mBAAa,IAAI,IAAI;AACrB,YAAM,IAAI,IAAI;AACd,YAAM,KAAK,IAAI;AAAA,IACjB;AAAA,EACF;AAEA,MAAI,uBAAuB;AAC3B,WAAS,MAAM,GAAG,MAAM,YAAY,OAAO;AACzC,QAAI,aAAa,GAAG,KAAK,oBAAoB,GAAG,EAAG,yBAAwB;AAAA,EAC7E;AACA,MAAI,wBAAwB,KAAK,uBAAuB,sBAAsB,KAAK,qBAAsB;AAEzG,QAAM,yBAAyB,IAAI,WAAW,UAAU,EAAE,KAAK,EAAE;AACjE,QAAM,eAAyB,CAAC;AAChC,WAAS,MAAM,GAAG,MAAM,YAAY,OAAO;AACzC,QAAI,CAAC,aAAa,GAAG,EAAG;AACxB,UAAM,IAAI,MAAM;AAChB,UAAM,IAAI,KAAK,MAAM,MAAM,KAAK;AAChC,UAAM,oBACH,IAAI,KAAK,CAAC,aAAa,MAAM,CAAC,KAC9B,IAAI,QAAQ,KAAK,CAAC,aAAa,MAAM,CAAC,KACtC,IAAI,KAAK,CAAC,aAAa,MAAM,KAAK,KAClC,IAAI,SAAS,KAAK,CAAC,aAAa,MAAM,KAAK;AAC9C,QAAI,mBAAmB;AACrB,6BAAuB,GAAG,IAAI;AAC9B,mBAAa,KAAK,GAAG;AAAA,IACvB;AAAA,EACF;AACA,MAAI,QAAQ;AACZ,SAAO,QAAQ,aAAa,QAAQ;AAClC,UAAM,MAAM,aAAa,KAAK,KAAK;AACnC,aAAS;AACT,UAAM,IAAI,uBAAuB,GAAG,KAAK;AACzC,QAAI,KAAK,KAAK,kBAAkB,EAAG;AACnC,UAAM,IAAI,MAAM;AAChB,UAAM,IAAI,KAAK,MAAM,MAAM,KAAK;AAChC,UAAM,YAAsB,CAAC;AAC7B,QAAI,IAAI,EAAG,WAAU,KAAK,MAAM,CAAC;AACjC,QAAI,IAAI,QAAQ,EAAG,WAAU,KAAK,MAAM,CAAC;AACzC,QAAI,IAAI,EAAG,WAAU,KAAK,MAAM,KAAK;AACrC,QAAI,IAAI,SAAS,EAAG,WAAU,KAAK,MAAM,KAAK;AAC9C,eAAW,QAAQ,WAAW;AAC5B,UAAI,CAAC,aAAa,IAAI,EAAG;AACzB,WAAK,uBAAuB,IAAI,KAAK,QAAQ,GAAI;AACjD,6BAAuB,IAAI,IAAI,IAAI;AACnC,mBAAa,KAAK,IAAI;AAAA,IACxB;AAAA,EACF;AAEA,WAAS,MAAM,GAAG,MAAM,YAAY,OAAO;AACzC,QAAI,CAAC,aAAa,GAAG,EAAG;AACxB,UAAM,IAAI,MAAM;AAChB,UAAM,IAAI,uBAAuB,GAAG,KAAK;AACzC,UAAM,YAAY,KAAK,IAAI,CAAC,KAAK;AACjC,SAAK,IAAI,CAAC,IAAI,MAAM,KAAK,IAAI,KAAK,MAAO,aAAa,KAAK,kBAAkB,MAAO,KAAK,kBAAkB,EAAE;AAAA,EAC/G;AAEA,MAAI,aAAa,WAAW,GAAG,CAAC;AAClC;;;AC1RO,IAAM,gBAAgB,CAAC,SAAiB,SAAiB,YAAoD;AAClH,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,MAAM,IAAI,MAAM;AACtB,QAAI,SAAS,MAAY;AACvB,YAAM,IAAI,IAAI;AACd,YAAM,IAAI,IAAI;AACd,UAAI,MAAM,KAAK,MAAM,GAAG;AACtB,gBAAQ,OAAO;AACf;AAAA,MACF;AACA,YAAM,SAAS,KAAK,IAAI,GAAG,CAAC;AAC5B,YAAM,QAAQ,SAAS,UAAU,UAAU,SAAS;AACpD,YAAM,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,KAAK,CAAC;AAC9C,YAAM,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,KAAK,CAAC;AAC9C,YAAM,OAAO,KAAK,IAAI,MAAM,IAAI;AAChC,YAAM,UAAU,KAAK,OAAO,OAAO,QAAQ,CAAC;AAC5C,YAAM,UAAU,KAAK,OAAO,OAAO,QAAQ,CAAC;AAC5C,YAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,aAAO,QAAQ;AACf,aAAO,SAAS;AAChB,YAAM,MAAM,OAAO,WAAW,MAAM,EAAE,oBAAoB,KAAK,CAAC;AAChE,UAAI,CAAC,KAAK;AACR,gBAAQ,OAAO;AACf;AAAA,MACF;AACA,UAAI,wBAAwB;AAC5B,UAAI,wBAAwB;AAC5B,UAAI,UAAU,KAAK,SAAS,SAAS,MAAM,IAAI;AAC/C,UAAI,SAAS,oBAAoB,OAAO;AACtC,4BAAoB,KAAK,MAAM,MAAM,OAAO;AAAA,MAC9C;AACA,cAAQ,OAAO,UAAU,WAAW,CAAC;AAAA,IACvC;AACA,QAAI,UAAU,MAAY,OAAO,IAAI,MAAM,QAAQ,CAAC;AACpD,QAAI,MAAM;AAAA,EACZ,CAAC;AACH;","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -15,4 +15,5 @@ type PrepareSpriteOptions = StripBackgroundOptions & {
|
|
|
15
15
|
};
|
|
16
16
|
declare const prepareSprite: (dataUrl: string, maxSide: number, options?: PrepareSpriteOptions) => Promise<string>;
|
|
17
17
|
|
|
18
|
-
export {
|
|
18
|
+
export { prepareSprite, stripEdgeBackground };
|
|
19
|
+
export type { PrepareSpriteOptions, StripBackgroundOptions };
|
package/dist/index.d.ts
CHANGED
|
@@ -15,4 +15,5 @@ type PrepareSpriteOptions = StripBackgroundOptions & {
|
|
|
15
15
|
};
|
|
16
16
|
declare const prepareSprite: (dataUrl: string, maxSide: number, options?: PrepareSpriteOptions) => Promise<string>;
|
|
17
17
|
|
|
18
|
-
export {
|
|
18
|
+
export { prepareSprite, stripEdgeBackground };
|
|
19
|
+
export type { PrepareSpriteOptions, StripBackgroundOptions };
|
package/dist/index.js
CHANGED
|
@@ -41,7 +41,16 @@ var isFlatImage = (data, pixelCount, coverage, quantLevels) => {
|
|
|
41
41
|
return (top1 + top2) / opaqueCount >= coverage;
|
|
42
42
|
};
|
|
43
43
|
var stripEdgeBackground = (ctx, width, height, options) => {
|
|
44
|
-
const opts = {
|
|
44
|
+
const opts = {
|
|
45
|
+
neutralChannelSpread: options?.neutralChannelSpread ?? DEFAULT_OPTIONS.neutralChannelSpread,
|
|
46
|
+
floodFillStepTolerance: options?.floodFillStepTolerance ?? DEFAULT_OPTIONS.floodFillStepTolerance,
|
|
47
|
+
floodFillMaxDrift: options?.floodFillMaxDrift ?? DEFAULT_OPTIONS.floodFillMaxDrift,
|
|
48
|
+
falloffRadiusPx: options?.falloffRadiusPx ?? DEFAULT_OPTIONS.falloffRadiusPx,
|
|
49
|
+
flatImageColorCoverage: options?.flatImageColorCoverage ?? DEFAULT_OPTIONS.flatImageColorCoverage,
|
|
50
|
+
flatImageColorQuantLevels: options?.flatImageColorQuantLevels ?? DEFAULT_OPTIONS.flatImageColorQuantLevels,
|
|
51
|
+
interiorBackgroundMatchTolerance: options?.interiorBackgroundMatchTolerance ?? DEFAULT_OPTIONS.interiorBackgroundMatchTolerance,
|
|
52
|
+
minStripAreaFraction: options?.minStripAreaFraction ?? DEFAULT_OPTIONS.minStripAreaFraction
|
|
53
|
+
};
|
|
45
54
|
const imageData = ctx.getImageData(0, 0, width, height);
|
|
46
55
|
const { data } = imageData;
|
|
47
56
|
const pixelCount = width * height;
|
|
@@ -87,19 +96,16 @@ var stripEdgeBackground = (ctx, width, height, options) => {
|
|
|
87
96
|
enqueueIfBackground(y * width);
|
|
88
97
|
enqueueIfBackground(y * width + (width - 1));
|
|
89
98
|
}
|
|
99
|
+
const bgAvgR = bgColorSeedCount > 0 ? bgColorSumR / bgColorSeedCount : 0;
|
|
100
|
+
const bgAvgG = bgColorSeedCount > 0 ? bgColorSumG / bgColorSeedCount : 0;
|
|
101
|
+
const bgAvgB = bgColorSeedCount > 0 ? bgColorSumB / bgColorSeedCount : 0;
|
|
102
|
+
const matchesLearnedBackground = (r, g, b) => bgColorSeedCount > 0 && Math.max(Math.abs(r - bgAvgR), Math.abs(g - bgAvgG), Math.abs(b - bgAvgB)) <= opts.interiorBackgroundMatchTolerance;
|
|
90
103
|
if (bgColorSeedCount > 0 && isFlatImage(data, pixelCount, opts.flatImageColorCoverage, opts.flatImageColorQuantLevels)) {
|
|
91
|
-
const bgAvgR = bgColorSumR / bgColorSeedCount;
|
|
92
|
-
const bgAvgG = bgColorSumG / bgColorSeedCount;
|
|
93
|
-
const bgAvgB = bgColorSumB / bgColorSeedCount;
|
|
94
104
|
for (let idx = 0; idx < pixelCount; idx++) {
|
|
95
105
|
if (seeded[idx]) continue;
|
|
96
106
|
const p = idx * 4;
|
|
97
107
|
if ((data[p + 3] ?? 0) === 0) continue;
|
|
98
|
-
|
|
99
|
-
const g = data[p + 1] ?? 0;
|
|
100
|
-
const b = data[p + 2] ?? 0;
|
|
101
|
-
const distance = Math.max(Math.abs(r - bgAvgR), Math.abs(g - bgAvgG), Math.abs(b - bgAvgB));
|
|
102
|
-
if (distance > opts.interiorBackgroundMatchTolerance) continue;
|
|
108
|
+
if (!matchesLearnedBackground(data[p] ?? 0, data[p + 1] ?? 0, data[p + 2] ?? 0)) continue;
|
|
103
109
|
seeded[idx] = 1;
|
|
104
110
|
isBackground[idx] = 1;
|
|
105
111
|
drift[idx] = 0;
|
|
@@ -122,6 +128,7 @@ var stripEdgeBackground = (ctx, width, height, options) => {
|
|
|
122
128
|
if (y > 0) neighbors.push(idx - width);
|
|
123
129
|
if (y < height - 1) neighbors.push(idx + width);
|
|
124
130
|
const currentDrift = drift[idx] ?? 0;
|
|
131
|
+
const fromTransparent = (data[p + 3] ?? 0) === 0;
|
|
125
132
|
for (const nIdx of neighbors) {
|
|
126
133
|
if (seeded[nIdx]) continue;
|
|
127
134
|
const np = nIdx * 4;
|
|
@@ -135,6 +142,14 @@ var stripEdgeBackground = (ctx, width, height, options) => {
|
|
|
135
142
|
const nr = data[np] ?? 0;
|
|
136
143
|
const ng = data[np + 1] ?? 0;
|
|
137
144
|
const nb = data[np + 2] ?? 0;
|
|
145
|
+
if (fromTransparent) {
|
|
146
|
+
if (!isNearNeutral(nr, ng, nb, opts.neutralChannelSpread) || !matchesLearnedBackground(nr, ng, nb)) continue;
|
|
147
|
+
seeded[nIdx] = 1;
|
|
148
|
+
isBackground[nIdx] = 1;
|
|
149
|
+
drift[nIdx] = 0;
|
|
150
|
+
queue.push(nIdx);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
138
153
|
const step = Math.max(Math.abs(nr - r0), Math.abs(ng - g0), Math.abs(nb - b0));
|
|
139
154
|
if (step > opts.floodFillStepTolerance) continue;
|
|
140
155
|
const newDrift = currentDrift + step;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/strip-background.ts","../src/prepare-sprite.ts"],"sourcesContent":["export type StripBackgroundOptions = {\n // Gates which edge pixels can seed the fill (near-black/gray/white only, so colored\n // backgrounds are left alone).\n neutralChannelSpread?: number\n // Bounds how far a pixel's color may drift from the already-filled neighbor that\n // reached it, so a single anti-aliased/noisy step gets absorbed but a hard-edge\n // outline stops the fill cold.\n floodFillStepTolerance?: number\n // Bounds the *total* drift accumulated over the whole chain of hops back to the\n // border seed — without it, many small sub-tolerance steps can chain together and\n // worm past a real outline into an interior region that happens to share a similar\n // tone.\n floodFillMaxDrift?: number\n // How many background pixels near the stopping edge get a soft alpha ramp instead\n // of a hard 0/255 cutoff.\n falloffRadiusPx?: number\n // Fraction of opaque pixels the two most common color buckets must cover for an\n // image to be treated as flat/bilevel line-art (only then is it safe to also seed\n // interior background holes — see isFlatImage).\n flatImageColorCoverage?: number\n // How finely colors are bucketed when checking flat-image coverage.\n flatImageColorQuantLevels?: number\n // How close an interior pixel must be to the *learned* border-background color (not\n // just \"near-neutral\") to be seeded directly on a flat image, so a flat image's\n // foreground color (also near-neutral, e.g. black ink) isn't swept up just for\n // being grayscale.\n interiorBackgroundMatchTolerance?: number\n // Below this fraction of the original opaque content, an entire strip result is\n // abandoned and the image is left untouched — guards against textured photo\n // backgrounds (a brick wall, grout lines) nibbling a small, irregular bite out of\n // one edge instead of either clearing the background cleanly or matching nothing.\n minStripAreaFraction?: number\n}\n\nconst DEFAULT_OPTIONS: Required<StripBackgroundOptions> = {\n neutralChannelSpread: 20,\n floodFillStepTolerance: 10,\n floodFillMaxDrift: 24,\n falloffRadiusPx: 3,\n flatImageColorCoverage: 0.92,\n flatImageColorQuantLevels: 32,\n interiorBackgroundMatchTolerance: 16,\n minStripAreaFraction: 0.03,\n}\n\nconst isNearNeutral = (r: number, g: number, b: number, spread: number): boolean => {\n const max = Math.max(r, g, b)\n const min = Math.min(r, g, b)\n return max - min <= spread\n}\n\n// Quantizes every opaque pixel's color and checks whether the two most common buckets\n// cover nearly the whole image — true for bilevel line art/logos/signatures, false for\n// photos and other continuous-tone images where a same-colored subject region could\n// plausibly exist.\nconst isFlatImage = (\n data: Uint8ClampedArray,\n pixelCount: number,\n coverage: number,\n quantLevels: number,\n): boolean => {\n const bucketCounts = new Map<number, number>()\n let opaqueCount = 0\n for (let idx = 0; idx < pixelCount; idx++) {\n const p = idx * 4\n if ((data[p + 3] ?? 0) === 0) continue\n opaqueCount += 1\n const rBucket = Math.floor(((data[p] ?? 0) / 256) * quantLevels)\n const gBucket = Math.floor(((data[p + 1] ?? 0) / 256) * quantLevels)\n const bBucket = Math.floor(((data[p + 2] ?? 0) / 256) * quantLevels)\n const key = (rBucket * quantLevels + gBucket) * quantLevels + bBucket\n bucketCounts.set(key, (bucketCounts.get(key) ?? 0) + 1)\n }\n if (opaqueCount === 0) return false\n let top1 = 0\n let top2 = 0\n for (const count of bucketCounts.values()) {\n if (count > top1) {\n top2 = top1\n top1 = count\n } else if (count > top2) {\n top2 = count\n }\n }\n return (top1 + top2) / opaqueCount >= coverage\n}\n\n// Flood-fills inward from the canvas edges, removing only pixels reachable from a\n// near-neutral border through a chain of locally-similar neighbors, then feathers the\n// resulting cut edge with a short alpha falloff instead of leaving a hard binary mask.\nexport const stripEdgeBackground = (\n ctx: CanvasRenderingContext2D,\n width: number,\n height: number,\n options?: StripBackgroundOptions,\n): void => {\n const opts = { ...DEFAULT_OPTIONS, ...options }\n const imageData = ctx.getImageData(0, 0, width, height)\n const { data } = imageData\n const pixelCount = width * height\n const isBackground = new Uint8Array(pixelCount)\n const seeded = new Uint8Array(pixelCount)\n const drift = new Uint16Array(pixelCount)\n const queue: number[] = []\n\n // Rectangular uploads may arrive already sitting on transparent square padding —\n // that padding is deliberate and shouldn't count as \"background removed\" one way or\n // the other, so the area-fraction sanity check below is measured only against pixels\n // that started out opaque (the real content), not the full padded canvas.\n const wasOriginallyOpaque = new Uint8Array(pixelCount)\n let originalOpaqueCount = 0\n for (let idx = 0; idx < pixelCount; idx++) {\n if ((data[idx * 4 + 3] ?? 0) === 0) continue\n wasOriginallyOpaque[idx] = 1\n originalOpaqueCount += 1\n }\n\n let bgColorSumR = 0\n let bgColorSumG = 0\n let bgColorSumB = 0\n let bgColorSeedCount = 0\n\n const enqueueIfBackground = (idx: number): void => {\n if (seeded[idx]) return\n const p = idx * 4\n const r = data[p] ?? 0\n const g = data[p + 1] ?? 0\n const b = data[p + 2] ?? 0\n const isTransparent = (data[p + 3] ?? 0) === 0\n if (!isTransparent && !isNearNeutral(r, g, b, opts.neutralChannelSpread)) return\n seeded[idx] = 1\n isBackground[idx] = 1\n drift[idx] = 0\n if (!isTransparent) {\n bgColorSumR += r\n bgColorSumG += g\n bgColorSumB += b\n bgColorSeedCount += 1\n }\n queue.push(idx)\n }\n\n for (let x = 0; x < width; x++) {\n enqueueIfBackground(x)\n enqueueIfBackground((height - 1) * width + x)\n }\n for (let y = 0; y < height; y++) {\n enqueueIfBackground(y * width)\n enqueueIfBackground(y * width + (width - 1))\n }\n\n // Flat/line-art images (few distinct colors, so no same-toned subject can plausibly\n // exist) also get interior pixels seeded directly whenever they closely match the\n // learned border-background color — this is what lets a fully-enclosed hole (e.g.\n // the loop of a cursive letter) get removed even though it never touches the canvas\n // edge. Photographic images skip this and keep the conservative border-only\n // behavior, since color alone can't tell a hole from a same-colored subject region\n // (an eye, a shirt) once ML-level semantics are needed.\n if (bgColorSeedCount > 0 && isFlatImage(data, pixelCount, opts.flatImageColorCoverage, opts.flatImageColorQuantLevels)) {\n const bgAvgR = bgColorSumR / bgColorSeedCount\n const bgAvgG = bgColorSumG / bgColorSeedCount\n const bgAvgB = bgColorSumB / bgColorSeedCount\n for (let idx = 0; idx < pixelCount; idx++) {\n if (seeded[idx]) continue\n const p = idx * 4\n if ((data[p + 3] ?? 0) === 0) continue\n const r = data[p] ?? 0\n const g = data[p + 1] ?? 0\n const b = data[p + 2] ?? 0\n const distance = Math.max(Math.abs(r - bgAvgR), Math.abs(g - bgAvgG), Math.abs(b - bgAvgB))\n if (distance > opts.interiorBackgroundMatchTolerance) continue\n seeded[idx] = 1\n isBackground[idx] = 1\n drift[idx] = 0\n queue.push(idx)\n }\n }\n\n let head = 0\n while (head < queue.length) {\n const idx = queue[head] ?? -1\n head += 1\n const x = idx % width\n const y = Math.floor(idx / width)\n const p = idx * 4\n const r0 = data[p] ?? 0\n const g0 = data[p + 1] ?? 0\n const b0 = data[p + 2] ?? 0\n\n const neighbors: number[] = []\n if (x > 0) neighbors.push(idx - 1)\n if (x < width - 1) neighbors.push(idx + 1)\n if (y > 0) neighbors.push(idx - width)\n if (y < height - 1) neighbors.push(idx + width)\n\n const currentDrift = drift[idx] ?? 0\n\n for (const nIdx of neighbors) {\n if (seeded[nIdx]) continue\n const np = nIdx * 4\n if ((data[np + 3] ?? 0) === 0) {\n seeded[nIdx] = 1\n isBackground[nIdx] = 1\n drift[nIdx] = currentDrift\n queue.push(nIdx)\n continue\n }\n const nr = data[np] ?? 0\n const ng = data[np + 1] ?? 0\n const nb = data[np + 2] ?? 0\n const step = Math.max(Math.abs(nr - r0), Math.abs(ng - g0), Math.abs(nb - b0))\n if (step > opts.floodFillStepTolerance) continue\n const newDrift = currentDrift + step\n if (newDrift > opts.floodFillMaxDrift) continue\n seeded[nIdx] = 1\n isBackground[nIdx] = 1\n drift[nIdx] = newDrift\n queue.push(nIdx)\n }\n }\n\n let strippedContentCount = 0\n for (let idx = 0; idx < pixelCount; idx++) {\n if (isBackground[idx] && wasOriginallyOpaque[idx]) strippedContentCount += 1\n }\n if (originalOpaqueCount === 0 || strippedContentCount / originalOpaqueCount < opts.minStripAreaFraction) return\n\n const distanceFromForeground = new Int16Array(pixelCount).fill(-1)\n const falloffQueue: number[] = []\n for (let idx = 0; idx < pixelCount; idx++) {\n if (!isBackground[idx]) continue\n const x = idx % width\n const y = Math.floor(idx / width)\n const touchesForeground =\n (x > 0 && !isBackground[idx - 1]) ||\n (x < width - 1 && !isBackground[idx + 1]) ||\n (y > 0 && !isBackground[idx - width]) ||\n (y < height - 1 && !isBackground[idx + width])\n if (touchesForeground) {\n distanceFromForeground[idx] = 0\n falloffQueue.push(idx)\n }\n }\n let fHead = 0\n while (fHead < falloffQueue.length) {\n const idx = falloffQueue[fHead] ?? -1\n fHead += 1\n const d = distanceFromForeground[idx] ?? -1\n if (d >= opts.falloffRadiusPx - 1) continue\n const x = idx % width\n const y = Math.floor(idx / width)\n const neighbors: number[] = []\n if (x > 0) neighbors.push(idx - 1)\n if (x < width - 1) neighbors.push(idx + 1)\n if (y > 0) neighbors.push(idx - width)\n if (y < height - 1) neighbors.push(idx + width)\n for (const nIdx of neighbors) {\n if (!isBackground[nIdx]) continue\n if ((distanceFromForeground[nIdx] ?? -1) !== -1) continue\n distanceFromForeground[nIdx] = d + 1\n falloffQueue.push(nIdx)\n }\n }\n\n for (let idx = 0; idx < pixelCount; idx++) {\n if (!isBackground[idx]) continue\n const p = idx * 4\n const d = distanceFromForeground[idx] ?? -1\n const origAlpha = data[p + 3] ?? 0\n data[p + 3] = d === -1 ? 0 : Math.round((origAlpha * (opts.falloffRadiusPx - d)) / (opts.falloffRadiusPx + 1))\n }\n\n ctx.putImageData(imageData, 0, 0)\n}\n","import { stripEdgeBackground, StripBackgroundOptions } from './strip-background'\n\nexport type PrepareSpriteOptions = StripBackgroundOptions & {\n // Set false to only resize/square-pad and skip the background strip step.\n stripBackground?: boolean\n}\n\n// Decodes a data URL, downscales it to fit maxSide if needed, and returns a new PNG\n// data URL. The particle/point-sprite renderers this was built for always billboard a\n// sprite as a square, so rectangular input is centered on a square transparent-padded\n// canvas rather than stretched — the padding absorbs the square billboard, and the\n// content keeps its original proportions. Background stripping (see stripEdgeBackground)\n// runs on the result unless disabled via options.\nexport const prepareSprite = (dataUrl: string, maxSide: number, options?: PrepareSpriteOptions): Promise<string> => {\n return new Promise((resolve, reject) => {\n const img = new Image()\n img.onload = (): void => {\n const w = img.naturalWidth\n const h = img.naturalHeight\n if (w === 0 || h === 0) {\n resolve(dataUrl)\n return\n }\n const maxDim = Math.max(w, h)\n const scale = maxDim > maxSide ? maxSide / maxDim : 1\n const newW = Math.max(1, Math.round(w * scale))\n const newH = Math.max(1, Math.round(h * scale))\n const side = Math.max(newW, newH)\n const offsetX = Math.floor((side - newW) / 2)\n const offsetY = Math.floor((side - newH) / 2)\n const canvas = document.createElement('canvas')\n canvas.width = side\n canvas.height = side\n const ctx = canvas.getContext('2d', { willReadFrequently: true })\n if (!ctx) {\n resolve(dataUrl)\n return\n }\n ctx.imageSmoothingEnabled = true\n ctx.imageSmoothingQuality = 'high'\n ctx.drawImage(img, offsetX, offsetY, newW, newH)\n if (options?.stripBackground !== false) {\n stripEdgeBackground(ctx, side, side, options)\n }\n resolve(canvas.toDataURL('image/png'))\n }\n img.onerror = (): void => reject(new Error('decode'))\n img.src = dataUrl\n })\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/strip-background.ts","../src/prepare-sprite.ts"],"sourcesContent":["export type StripBackgroundOptions = {\n // Gates which edge pixels can seed the fill (near-black/gray/white only, so colored\n // backgrounds are left alone).\n neutralChannelSpread?: number\n // Bounds how far a pixel's color may drift from the already-filled neighbor that\n // reached it, so a single anti-aliased/noisy step gets absorbed but a hard-edge\n // outline stops the fill cold.\n floodFillStepTolerance?: number\n // Bounds the *total* drift accumulated over the whole chain of hops back to the\n // border seed — without it, many small sub-tolerance steps can chain together and\n // worm past a real outline into an interior region that happens to share a similar\n // tone.\n floodFillMaxDrift?: number\n // How many background pixels near the stopping edge get a soft alpha ramp instead\n // of a hard 0/255 cutoff.\n falloffRadiusPx?: number\n // Fraction of opaque pixels the two most common color buckets must cover for an\n // image to be treated as flat/bilevel line-art (only then is it safe to also seed\n // interior background holes — see isFlatImage).\n flatImageColorCoverage?: number\n // How finely colors are bucketed when checking flat-image coverage.\n flatImageColorQuantLevels?: number\n // How close an interior pixel must be to the *learned* border-background color (not\n // just \"near-neutral\") to be seeded directly on a flat image, so a flat image's\n // foreground color (also near-neutral, e.g. black ink) isn't swept up just for\n // being grayscale.\n interiorBackgroundMatchTolerance?: number\n // Below this fraction of the original opaque content, an entire strip result is\n // abandoned and the image is left untouched — guards against textured photo\n // backgrounds (a brick wall, grout lines) nibbling a small, irregular bite out of\n // one edge instead of either clearing the background cleanly or matching nothing.\n minStripAreaFraction?: number\n}\n\nconst DEFAULT_OPTIONS: Required<StripBackgroundOptions> = {\n neutralChannelSpread: 20,\n floodFillStepTolerance: 10,\n floodFillMaxDrift: 24,\n falloffRadiusPx: 3,\n flatImageColorCoverage: 0.92,\n flatImageColorQuantLevels: 32,\n interiorBackgroundMatchTolerance: 16,\n minStripAreaFraction: 0.03,\n}\n\nconst isNearNeutral = (r: number, g: number, b: number, spread: number): boolean => {\n const max = Math.max(r, g, b)\n const min = Math.min(r, g, b)\n return max - min <= spread\n}\n\n// Quantizes every opaque pixel's color and checks whether the two most common buckets\n// cover nearly the whole image — true for bilevel line art/logos/signatures, false for\n// photos and other continuous-tone images where a same-colored subject region could\n// plausibly exist.\nconst isFlatImage = (\n data: Uint8ClampedArray,\n pixelCount: number,\n coverage: number,\n quantLevels: number,\n): boolean => {\n const bucketCounts = new Map<number, number>()\n let opaqueCount = 0\n for (let idx = 0; idx < pixelCount; idx++) {\n const p = idx * 4\n if ((data[p + 3] ?? 0) === 0) continue\n opaqueCount += 1\n const rBucket = Math.floor(((data[p] ?? 0) / 256) * quantLevels)\n const gBucket = Math.floor(((data[p + 1] ?? 0) / 256) * quantLevels)\n const bBucket = Math.floor(((data[p + 2] ?? 0) / 256) * quantLevels)\n const key = (rBucket * quantLevels + gBucket) * quantLevels + bBucket\n bucketCounts.set(key, (bucketCounts.get(key) ?? 0) + 1)\n }\n if (opaqueCount === 0) return false\n let top1 = 0\n let top2 = 0\n for (const count of bucketCounts.values()) {\n if (count > top1) {\n top2 = top1\n top1 = count\n } else if (count > top2) {\n top2 = count\n }\n }\n return (top1 + top2) / opaqueCount >= coverage\n}\n\n// Flood-fills inward from the canvas edges, removing only pixels reachable from a\n// near-neutral border through a chain of locally-similar neighbors, then feathers the\n// resulting cut edge with a short alpha falloff instead of leaving a hard binary mask.\nexport const stripEdgeBackground = (\n ctx: CanvasRenderingContext2D,\n width: number,\n height: number,\n options?: StripBackgroundOptions,\n): void => {\n // Per field, so an option passed as `undefined` keeps its default instead of replacing it.\n const opts: Required<StripBackgroundOptions> = {\n neutralChannelSpread: options?.neutralChannelSpread ?? DEFAULT_OPTIONS.neutralChannelSpread,\n floodFillStepTolerance: options?.floodFillStepTolerance ?? DEFAULT_OPTIONS.floodFillStepTolerance,\n floodFillMaxDrift: options?.floodFillMaxDrift ?? DEFAULT_OPTIONS.floodFillMaxDrift,\n falloffRadiusPx: options?.falloffRadiusPx ?? DEFAULT_OPTIONS.falloffRadiusPx,\n flatImageColorCoverage: options?.flatImageColorCoverage ?? DEFAULT_OPTIONS.flatImageColorCoverage,\n flatImageColorQuantLevels: options?.flatImageColorQuantLevels ?? DEFAULT_OPTIONS.flatImageColorQuantLevels,\n interiorBackgroundMatchTolerance: options?.interiorBackgroundMatchTolerance ?? DEFAULT_OPTIONS.interiorBackgroundMatchTolerance,\n minStripAreaFraction: options?.minStripAreaFraction ?? DEFAULT_OPTIONS.minStripAreaFraction,\n }\n const imageData = ctx.getImageData(0, 0, width, height)\n const { data } = imageData\n const pixelCount = width * height\n const isBackground = new Uint8Array(pixelCount)\n const seeded = new Uint8Array(pixelCount)\n const drift = new Uint16Array(pixelCount)\n const queue: number[] = []\n\n // Rectangular uploads may arrive already sitting on transparent square padding —\n // that padding is deliberate and shouldn't count as \"background removed\" one way or\n // the other, so the area-fraction sanity check below is measured only against pixels\n // that started out opaque (the real content), not the full padded canvas.\n const wasOriginallyOpaque = new Uint8Array(pixelCount)\n let originalOpaqueCount = 0\n for (let idx = 0; idx < pixelCount; idx++) {\n if ((data[idx * 4 + 3] ?? 0) === 0) continue\n wasOriginallyOpaque[idx] = 1\n originalOpaqueCount += 1\n }\n\n let bgColorSumR = 0\n let bgColorSumG = 0\n let bgColorSumB = 0\n let bgColorSeedCount = 0\n\n const enqueueIfBackground = (idx: number): void => {\n if (seeded[idx]) return\n const p = idx * 4\n const r = data[p] ?? 0\n const g = data[p + 1] ?? 0\n const b = data[p + 2] ?? 0\n const isTransparent = (data[p + 3] ?? 0) === 0\n if (!isTransparent && !isNearNeutral(r, g, b, opts.neutralChannelSpread)) return\n seeded[idx] = 1\n isBackground[idx] = 1\n drift[idx] = 0\n if (!isTransparent) {\n bgColorSumR += r\n bgColorSumG += g\n bgColorSumB += b\n bgColorSeedCount += 1\n }\n queue.push(idx)\n }\n\n for (let x = 0; x < width; x++) {\n enqueueIfBackground(x)\n enqueueIfBackground((height - 1) * width + x)\n }\n for (let y = 0; y < height; y++) {\n enqueueIfBackground(y * width)\n enqueueIfBackground(y * width + (width - 1))\n }\n\n // Flat/line-art images (few distinct colors, so no same-toned subject can plausibly\n // exist) also get interior pixels seeded directly whenever they closely match the\n // learned border-background color — this is what lets a fully-enclosed hole (e.g.\n // the loop of a cursive letter) get removed even though it never touches the canvas\n // edge. Photographic images skip this and keep the conservative border-only\n // behavior, since color alone can't tell a hole from a same-colored subject region\n // (an eye, a shirt) once ML-level semantics are needed.\n const bgAvgR = bgColorSeedCount > 0 ? bgColorSumR / bgColorSeedCount : 0\n const bgAvgG = bgColorSeedCount > 0 ? bgColorSumG / bgColorSeedCount : 0\n const bgAvgB = bgColorSeedCount > 0 ? bgColorSumB / bgColorSeedCount : 0\n // Close to the background color learned from the opaque border pixels (never true when the border was all transparent).\n const matchesLearnedBackground = (r: number, g: number, b: number): boolean =>\n bgColorSeedCount > 0 &&\n Math.max(Math.abs(r - bgAvgR), Math.abs(g - bgAvgG), Math.abs(b - bgAvgB)) <= opts.interiorBackgroundMatchTolerance\n\n if (bgColorSeedCount > 0 && isFlatImage(data, pixelCount, opts.flatImageColorCoverage, opts.flatImageColorQuantLevels)) {\n for (let idx = 0; idx < pixelCount; idx++) {\n if (seeded[idx]) continue\n const p = idx * 4\n if ((data[p + 3] ?? 0) === 0) continue\n if (!matchesLearnedBackground(data[p] ?? 0, data[p + 1] ?? 0, data[p + 2] ?? 0)) continue\n seeded[idx] = 1\n isBackground[idx] = 1\n drift[idx] = 0\n queue.push(idx)\n }\n }\n\n let head = 0\n while (head < queue.length) {\n const idx = queue[head] ?? -1\n head += 1\n const x = idx % width\n const y = Math.floor(idx / width)\n const p = idx * 4\n const r0 = data[p] ?? 0\n const g0 = data[p + 1] ?? 0\n const b0 = data[p + 2] ?? 0\n\n const neighbors: number[] = []\n if (x > 0) neighbors.push(idx - 1)\n if (x < width - 1) neighbors.push(idx + 1)\n if (y > 0) neighbors.push(idx - width)\n if (y < height - 1) neighbors.push(idx + width)\n\n const currentDrift = drift[idx] ?? 0\n // A transparent pixel's RGB means nothing (usually 0,0,0), so stepping from padding into content is not a color step: it takes an\n // opaque pixel only when it reads as the background itself, the way a border seed would, and the drift starts over there.\n const fromTransparent = (data[p + 3] ?? 0) === 0\n\n for (const nIdx of neighbors) {\n if (seeded[nIdx]) continue\n const np = nIdx * 4\n if ((data[np + 3] ?? 0) === 0) {\n seeded[nIdx] = 1\n isBackground[nIdx] = 1\n drift[nIdx] = currentDrift\n queue.push(nIdx)\n continue\n }\n const nr = data[np] ?? 0\n const ng = data[np + 1] ?? 0\n const nb = data[np + 2] ?? 0\n if (fromTransparent) {\n if (!isNearNeutral(nr, ng, nb, opts.neutralChannelSpread) || !matchesLearnedBackground(nr, ng, nb)) continue\n seeded[nIdx] = 1\n isBackground[nIdx] = 1\n drift[nIdx] = 0\n queue.push(nIdx)\n continue\n }\n const step = Math.max(Math.abs(nr - r0), Math.abs(ng - g0), Math.abs(nb - b0))\n if (step > opts.floodFillStepTolerance) continue\n const newDrift = currentDrift + step\n if (newDrift > opts.floodFillMaxDrift) continue\n seeded[nIdx] = 1\n isBackground[nIdx] = 1\n drift[nIdx] = newDrift\n queue.push(nIdx)\n }\n }\n\n let strippedContentCount = 0\n for (let idx = 0; idx < pixelCount; idx++) {\n if (isBackground[idx] && wasOriginallyOpaque[idx]) strippedContentCount += 1\n }\n if (originalOpaqueCount === 0 || strippedContentCount / originalOpaqueCount < opts.minStripAreaFraction) return\n\n const distanceFromForeground = new Int16Array(pixelCount).fill(-1)\n const falloffQueue: number[] = []\n for (let idx = 0; idx < pixelCount; idx++) {\n if (!isBackground[idx]) continue\n const x = idx % width\n const y = Math.floor(idx / width)\n const touchesForeground =\n (x > 0 && !isBackground[idx - 1]) ||\n (x < width - 1 && !isBackground[idx + 1]) ||\n (y > 0 && !isBackground[idx - width]) ||\n (y < height - 1 && !isBackground[idx + width])\n if (touchesForeground) {\n distanceFromForeground[idx] = 0\n falloffQueue.push(idx)\n }\n }\n let fHead = 0\n while (fHead < falloffQueue.length) {\n const idx = falloffQueue[fHead] ?? -1\n fHead += 1\n const d = distanceFromForeground[idx] ?? -1\n if (d >= opts.falloffRadiusPx - 1) continue\n const x = idx % width\n const y = Math.floor(idx / width)\n const neighbors: number[] = []\n if (x > 0) neighbors.push(idx - 1)\n if (x < width - 1) neighbors.push(idx + 1)\n if (y > 0) neighbors.push(idx - width)\n if (y < height - 1) neighbors.push(idx + width)\n for (const nIdx of neighbors) {\n if (!isBackground[nIdx]) continue\n if ((distanceFromForeground[nIdx] ?? -1) !== -1) continue\n distanceFromForeground[nIdx] = d + 1\n falloffQueue.push(nIdx)\n }\n }\n\n for (let idx = 0; idx < pixelCount; idx++) {\n if (!isBackground[idx]) continue\n const p = idx * 4\n const d = distanceFromForeground[idx] ?? -1\n const origAlpha = data[p + 3] ?? 0\n data[p + 3] = d === -1 ? 0 : Math.round((origAlpha * (opts.falloffRadiusPx - d)) / (opts.falloffRadiusPx + 1))\n }\n\n ctx.putImageData(imageData, 0, 0)\n}\n","import { stripEdgeBackground, StripBackgroundOptions } from './strip-background'\n\nexport type PrepareSpriteOptions = StripBackgroundOptions & {\n // Set false to only resize/square-pad and skip the background strip step.\n stripBackground?: boolean\n}\n\n// Decodes a data URL, downscales it to fit maxSide if needed, and returns a new PNG\n// data URL. The particle/point-sprite renderers this was built for always billboard a\n// sprite as a square, so rectangular input is centered on a square transparent-padded\n// canvas rather than stretched — the padding absorbs the square billboard, and the\n// content keeps its original proportions. Background stripping (see stripEdgeBackground)\n// runs on the result unless disabled via options.\nexport const prepareSprite = (dataUrl: string, maxSide: number, options?: PrepareSpriteOptions): Promise<string> => {\n return new Promise((resolve, reject) => {\n const img = new Image()\n img.onload = (): void => {\n const w = img.naturalWidth\n const h = img.naturalHeight\n if (w === 0 || h === 0) {\n resolve(dataUrl)\n return\n }\n const maxDim = Math.max(w, h)\n const scale = maxDim > maxSide ? maxSide / maxDim : 1\n const newW = Math.max(1, Math.round(w * scale))\n const newH = Math.max(1, Math.round(h * scale))\n const side = Math.max(newW, newH)\n const offsetX = Math.floor((side - newW) / 2)\n const offsetY = Math.floor((side - newH) / 2)\n const canvas = document.createElement('canvas')\n canvas.width = side\n canvas.height = side\n const ctx = canvas.getContext('2d', { willReadFrequently: true })\n if (!ctx) {\n resolve(dataUrl)\n return\n }\n ctx.imageSmoothingEnabled = true\n ctx.imageSmoothingQuality = 'high'\n ctx.drawImage(img, offsetX, offsetY, newW, newH)\n if (options?.stripBackground !== false) {\n stripEdgeBackground(ctx, side, side, options)\n }\n resolve(canvas.toDataURL('image/png'))\n }\n img.onerror = (): void => reject(new Error('decode'))\n img.src = dataUrl\n })\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprite-strip",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Edge-seeded flood-fill background removal and square-pad resize for sprite images, via browser Canvas 2D.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
|
+
"@fractaleyez/source": "./src/index.ts",
|
|
11
12
|
"types": "./dist/index.d.ts",
|
|
12
13
|
"import": "./dist/index.js",
|
|
13
14
|
"require": "./dist/index.cjs"
|
|
@@ -18,11 +19,11 @@
|
|
|
18
19
|
],
|
|
19
20
|
"sideEffects": false,
|
|
20
21
|
"scripts": {
|
|
21
|
-
"build": "tsup
|
|
22
|
+
"build": "tsup",
|
|
22
23
|
"typecheck": "tsc --noEmit",
|
|
23
24
|
"lint": "eslint src test --fix",
|
|
24
25
|
"test": "vitest run",
|
|
25
|
-
"prepublishOnly": "
|
|
26
|
+
"prepublishOnly": "npm run build"
|
|
26
27
|
},
|
|
27
28
|
"keywords": [
|
|
28
29
|
"canvas",
|
|
@@ -35,15 +36,19 @@
|
|
|
35
36
|
"license": "MIT",
|
|
36
37
|
"repository": {
|
|
37
38
|
"type": "git",
|
|
38
|
-
"url": "git+https://github.com/c0d3ster/
|
|
39
|
+
"url": "git+https://github.com/c0d3ster/Fractaleyez.git",
|
|
40
|
+
"directory": "packages/sprite-strip"
|
|
39
41
|
},
|
|
40
42
|
"devDependencies": {
|
|
41
43
|
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
|
42
44
|
"@typescript-eslint/parser": "^8.18.0",
|
|
43
45
|
"eslint": "^9.17.0",
|
|
44
46
|
"eslint-plugin-prefer-arrow": "^1.2.3",
|
|
45
|
-
"tsup": "^
|
|
46
|
-
"typescript": "^
|
|
47
|
-
"vitest": "^
|
|
47
|
+
"tsup": "^9.0.0",
|
|
48
|
+
"typescript": "^6.0.2",
|
|
49
|
+
"vitest": "^4.1.11"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
48
53
|
}
|
|
49
54
|
}
|