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 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 = { ...DEFAULT_OPTIONS, ...options };
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
- const r = data[p] ?? 0;
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;
@@ -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":"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","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 { type PrepareSpriteOptions, type StripBackgroundOptions, prepareSprite, stripEdgeBackground };
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 { type PrepareSpriteOptions, type StripBackgroundOptions, prepareSprite, stripEdgeBackground };
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 = { ...DEFAULT_OPTIONS, ...options };
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
- const r = data[p] ?? 0;
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.0",
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 src/index.ts --format esm,cjs --dts",
22
+ "build": "tsup",
22
23
  "typecheck": "tsc --noEmit",
23
24
  "lint": "eslint src test --fix",
24
25
  "test": "vitest run",
25
- "prepublishOnly": "yarn build"
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/sprite-strip.git"
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": "^8.3.5",
46
- "typescript": "^5.7.2",
47
- "vitest": "^2.1.8"
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
  }