sprite-strip 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/index.cjs +265 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +18 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +237 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cody Douglass
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# sprite-strip
|
|
2
|
+
|
|
3
|
+
Edge-seeded flood-fill background removal and square-pad resize for sprite images,
|
|
4
|
+
built on the browser Canvas 2D API. No dependencies.
|
|
5
|
+
|
|
6
|
+
It exists because point-sprite/particle renderers (`THREE.Points` and similar) always
|
|
7
|
+
billboard a sprite texture as a square — a rectangular upload gets stretched to fill
|
|
8
|
+
that square unless you pad it first. `prepareSprite` handles the resize/pad and, by
|
|
9
|
+
default, also strips a flat-colored background so the sprite reads as a clean cutout
|
|
10
|
+
instead of a colored square.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install sprite-strip
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
The common case — take a user-uploaded image, cap it at a max side length, strip a
|
|
21
|
+
neutral background, get back a PNG data URL:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { prepareSprite } from 'sprite-strip'
|
|
25
|
+
|
|
26
|
+
const processed = await prepareSprite(rawDataUrl, 512)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Skip the background strip and only resize/pad:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const processed = await prepareSprite(rawDataUrl, 512, { stripBackground: false })
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Run the strip directly against a canvas you already control:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { stripEdgeBackground } from 'sprite-strip'
|
|
39
|
+
|
|
40
|
+
const ctx = canvas.getContext('2d')
|
|
41
|
+
stripEdgeBackground(ctx, canvas.width, canvas.height)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## How it works
|
|
45
|
+
|
|
46
|
+
`stripEdgeBackground` flood-fills inward from the canvas border, seeding only from
|
|
47
|
+
near-neutral (black/gray/white) edge pixels so colored backgrounds are left untouched.
|
|
48
|
+
Propagation compares each pixel to the already-filled neighbor that reached it (not
|
|
49
|
+
the original seed), so gradual gradients/noise get absorbed but a hard-edge outline
|
|
50
|
+
stops the fill. If the whole image is flat, bilevel line-art (few distinct colors —
|
|
51
|
+
logos, signatures), fully-enclosed background-colored holes (e.g. the loop of a
|
|
52
|
+
cursive letter) are also removed, since color alone can't safely make that call on a
|
|
53
|
+
photo where a same-toned subject region might exist. Background pixels near the
|
|
54
|
+
resulting cut boundary get a short alpha falloff instead of a hard 0/255 edge.
|
|
55
|
+
|
|
56
|
+
If the stripped area comes out too small relative to the original opaque content, the
|
|
57
|
+
whole strip is abandoned and the image is returned untouched — this guards against
|
|
58
|
+
textured photo backgrounds (a brick wall, grout lines) nibbling a small, irregular
|
|
59
|
+
bite out of one edge instead of matching cleanly or not at all.
|
|
60
|
+
|
|
61
|
+
## API
|
|
62
|
+
|
|
63
|
+
### `prepareSprite(dataUrl: string, maxSide: number, options?: PrepareSpriteOptions): Promise<string>`
|
|
64
|
+
|
|
65
|
+
Decodes `dataUrl`, downscales to fit `maxSide` if needed, centers the result on a
|
|
66
|
+
square transparent-padded canvas, strips the background (unless
|
|
67
|
+
`options.stripBackground` is `false`), and resolves to a PNG data URL.
|
|
68
|
+
|
|
69
|
+
### `stripEdgeBackground(ctx: CanvasRenderingContext2D, width: number, height: number, options?: StripBackgroundOptions): void`
|
|
70
|
+
|
|
71
|
+
Runs the background strip in place against an existing canvas context.
|
|
72
|
+
|
|
73
|
+
### `StripBackgroundOptions`
|
|
74
|
+
|
|
75
|
+
All fields optional; every default below was tuned against real uploaded sprites.
|
|
76
|
+
|
|
77
|
+
| Option | Default | Purpose |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| `neutralChannelSpread` | `20` | Max channel spread for an edge pixel to seed the fill (near-neutral gate) |
|
|
80
|
+
| `floodFillStepTolerance` | `10` | Max color drift allowed for a single hop to a neighboring pixel |
|
|
81
|
+
| `floodFillMaxDrift` | `24` | Max *cumulative* drift allowed over the whole chain back to the border seed |
|
|
82
|
+
| `falloffRadiusPx` | `3` | Width of the soft alpha ramp at the stripped edge |
|
|
83
|
+
| `flatImageColorCoverage` | `0.92` | Coverage threshold for treating an image as flat/bilevel line-art |
|
|
84
|
+
| `flatImageColorQuantLevels` | `32` | Color quantization granularity used for the flat-image check |
|
|
85
|
+
| `interiorBackgroundMatchTolerance` | `16` | Color distance from the learned background color for seeding an interior hole |
|
|
86
|
+
| `minStripAreaFraction` | `0.03` | Minimum stripped fraction of original opaque content, below which the strip is abandoned |
|
|
87
|
+
|
|
88
|
+
`PrepareSpriteOptions` extends `StripBackgroundOptions` with:
|
|
89
|
+
|
|
90
|
+
| Option | Default | Purpose |
|
|
91
|
+
| --- | --- | --- |
|
|
92
|
+
| `stripBackground` | `true` | Set `false` to only resize/pad and skip the background strip |
|
|
93
|
+
|
|
94
|
+
## Scope
|
|
95
|
+
|
|
96
|
+
Browser only — `prepareSprite` uses `Image`, `document.createElement('canvas')`, and
|
|
97
|
+
`canvas.toDataURL`. `stripEdgeBackground` itself only calls `getImageData`/
|
|
98
|
+
`putImageData` on the context you pass it, so it could work with a non-DOM canvas
|
|
99
|
+
implementation, but that's untested and unsupported for now.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
prepareSprite: () => prepareSprite,
|
|
24
|
+
stripEdgeBackground: () => stripEdgeBackground
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/strip-background.ts
|
|
29
|
+
var DEFAULT_OPTIONS = {
|
|
30
|
+
neutralChannelSpread: 20,
|
|
31
|
+
floodFillStepTolerance: 10,
|
|
32
|
+
floodFillMaxDrift: 24,
|
|
33
|
+
falloffRadiusPx: 3,
|
|
34
|
+
flatImageColorCoverage: 0.92,
|
|
35
|
+
flatImageColorQuantLevels: 32,
|
|
36
|
+
interiorBackgroundMatchTolerance: 16,
|
|
37
|
+
minStripAreaFraction: 0.03
|
|
38
|
+
};
|
|
39
|
+
var isNearNeutral = (r, g, b, spread) => {
|
|
40
|
+
const max = Math.max(r, g, b);
|
|
41
|
+
const min = Math.min(r, g, b);
|
|
42
|
+
return max - min <= spread;
|
|
43
|
+
};
|
|
44
|
+
var isFlatImage = (data, pixelCount, coverage, quantLevels) => {
|
|
45
|
+
const bucketCounts = /* @__PURE__ */ new Map();
|
|
46
|
+
let opaqueCount = 0;
|
|
47
|
+
for (let idx = 0; idx < pixelCount; idx++) {
|
|
48
|
+
const p = idx * 4;
|
|
49
|
+
if ((data[p + 3] ?? 0) === 0) continue;
|
|
50
|
+
opaqueCount += 1;
|
|
51
|
+
const rBucket = Math.floor((data[p] ?? 0) / 256 * quantLevels);
|
|
52
|
+
const gBucket = Math.floor((data[p + 1] ?? 0) / 256 * quantLevels);
|
|
53
|
+
const bBucket = Math.floor((data[p + 2] ?? 0) / 256 * quantLevels);
|
|
54
|
+
const key = (rBucket * quantLevels + gBucket) * quantLevels + bBucket;
|
|
55
|
+
bucketCounts.set(key, (bucketCounts.get(key) ?? 0) + 1);
|
|
56
|
+
}
|
|
57
|
+
if (opaqueCount === 0) return false;
|
|
58
|
+
let top1 = 0;
|
|
59
|
+
let top2 = 0;
|
|
60
|
+
for (const count of bucketCounts.values()) {
|
|
61
|
+
if (count > top1) {
|
|
62
|
+
top2 = top1;
|
|
63
|
+
top1 = count;
|
|
64
|
+
} else if (count > top2) {
|
|
65
|
+
top2 = count;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return (top1 + top2) / opaqueCount >= coverage;
|
|
69
|
+
};
|
|
70
|
+
var stripEdgeBackground = (ctx, width, height, options) => {
|
|
71
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
72
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
73
|
+
const { data } = imageData;
|
|
74
|
+
const pixelCount = width * height;
|
|
75
|
+
const isBackground = new Uint8Array(pixelCount);
|
|
76
|
+
const seeded = new Uint8Array(pixelCount);
|
|
77
|
+
const drift = new Uint16Array(pixelCount);
|
|
78
|
+
const queue = [];
|
|
79
|
+
const wasOriginallyOpaque = new Uint8Array(pixelCount);
|
|
80
|
+
let originalOpaqueCount = 0;
|
|
81
|
+
for (let idx = 0; idx < pixelCount; idx++) {
|
|
82
|
+
if ((data[idx * 4 + 3] ?? 0) === 0) continue;
|
|
83
|
+
wasOriginallyOpaque[idx] = 1;
|
|
84
|
+
originalOpaqueCount += 1;
|
|
85
|
+
}
|
|
86
|
+
let bgColorSumR = 0;
|
|
87
|
+
let bgColorSumG = 0;
|
|
88
|
+
let bgColorSumB = 0;
|
|
89
|
+
let bgColorSeedCount = 0;
|
|
90
|
+
const enqueueIfBackground = (idx) => {
|
|
91
|
+
if (seeded[idx]) return;
|
|
92
|
+
const p = idx * 4;
|
|
93
|
+
const r = data[p] ?? 0;
|
|
94
|
+
const g = data[p + 1] ?? 0;
|
|
95
|
+
const b = data[p + 2] ?? 0;
|
|
96
|
+
const isTransparent = (data[p + 3] ?? 0) === 0;
|
|
97
|
+
if (!isTransparent && !isNearNeutral(r, g, b, opts.neutralChannelSpread)) return;
|
|
98
|
+
seeded[idx] = 1;
|
|
99
|
+
isBackground[idx] = 1;
|
|
100
|
+
drift[idx] = 0;
|
|
101
|
+
if (!isTransparent) {
|
|
102
|
+
bgColorSumR += r;
|
|
103
|
+
bgColorSumG += g;
|
|
104
|
+
bgColorSumB += b;
|
|
105
|
+
bgColorSeedCount += 1;
|
|
106
|
+
}
|
|
107
|
+
queue.push(idx);
|
|
108
|
+
};
|
|
109
|
+
for (let x = 0; x < width; x++) {
|
|
110
|
+
enqueueIfBackground(x);
|
|
111
|
+
enqueueIfBackground((height - 1) * width + x);
|
|
112
|
+
}
|
|
113
|
+
for (let y = 0; y < height; y++) {
|
|
114
|
+
enqueueIfBackground(y * width);
|
|
115
|
+
enqueueIfBackground(y * width + (width - 1));
|
|
116
|
+
}
|
|
117
|
+
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
|
+
for (let idx = 0; idx < pixelCount; idx++) {
|
|
122
|
+
if (seeded[idx]) continue;
|
|
123
|
+
const p = idx * 4;
|
|
124
|
+
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;
|
|
130
|
+
seeded[idx] = 1;
|
|
131
|
+
isBackground[idx] = 1;
|
|
132
|
+
drift[idx] = 0;
|
|
133
|
+
queue.push(idx);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
let head = 0;
|
|
137
|
+
while (head < queue.length) {
|
|
138
|
+
const idx = queue[head] ?? -1;
|
|
139
|
+
head += 1;
|
|
140
|
+
const x = idx % width;
|
|
141
|
+
const y = Math.floor(idx / width);
|
|
142
|
+
const p = idx * 4;
|
|
143
|
+
const r0 = data[p] ?? 0;
|
|
144
|
+
const g0 = data[p + 1] ?? 0;
|
|
145
|
+
const b0 = data[p + 2] ?? 0;
|
|
146
|
+
const neighbors = [];
|
|
147
|
+
if (x > 0) neighbors.push(idx - 1);
|
|
148
|
+
if (x < width - 1) neighbors.push(idx + 1);
|
|
149
|
+
if (y > 0) neighbors.push(idx - width);
|
|
150
|
+
if (y < height - 1) neighbors.push(idx + width);
|
|
151
|
+
const currentDrift = drift[idx] ?? 0;
|
|
152
|
+
for (const nIdx of neighbors) {
|
|
153
|
+
if (seeded[nIdx]) continue;
|
|
154
|
+
const np = nIdx * 4;
|
|
155
|
+
if ((data[np + 3] ?? 0) === 0) {
|
|
156
|
+
seeded[nIdx] = 1;
|
|
157
|
+
isBackground[nIdx] = 1;
|
|
158
|
+
drift[nIdx] = currentDrift;
|
|
159
|
+
queue.push(nIdx);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const nr = data[np] ?? 0;
|
|
163
|
+
const ng = data[np + 1] ?? 0;
|
|
164
|
+
const nb = data[np + 2] ?? 0;
|
|
165
|
+
const step = Math.max(Math.abs(nr - r0), Math.abs(ng - g0), Math.abs(nb - b0));
|
|
166
|
+
if (step > opts.floodFillStepTolerance) continue;
|
|
167
|
+
const newDrift = currentDrift + step;
|
|
168
|
+
if (newDrift > opts.floodFillMaxDrift) continue;
|
|
169
|
+
seeded[nIdx] = 1;
|
|
170
|
+
isBackground[nIdx] = 1;
|
|
171
|
+
drift[nIdx] = newDrift;
|
|
172
|
+
queue.push(nIdx);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
let strippedContentCount = 0;
|
|
176
|
+
for (let idx = 0; idx < pixelCount; idx++) {
|
|
177
|
+
if (isBackground[idx] && wasOriginallyOpaque[idx]) strippedContentCount += 1;
|
|
178
|
+
}
|
|
179
|
+
if (originalOpaqueCount === 0 || strippedContentCount / originalOpaqueCount < opts.minStripAreaFraction) return;
|
|
180
|
+
const distanceFromForeground = new Int16Array(pixelCount).fill(-1);
|
|
181
|
+
const falloffQueue = [];
|
|
182
|
+
for (let idx = 0; idx < pixelCount; idx++) {
|
|
183
|
+
if (!isBackground[idx]) continue;
|
|
184
|
+
const x = idx % width;
|
|
185
|
+
const y = Math.floor(idx / width);
|
|
186
|
+
const touchesForeground = x > 0 && !isBackground[idx - 1] || x < width - 1 && !isBackground[idx + 1] || y > 0 && !isBackground[idx - width] || y < height - 1 && !isBackground[idx + width];
|
|
187
|
+
if (touchesForeground) {
|
|
188
|
+
distanceFromForeground[idx] = 0;
|
|
189
|
+
falloffQueue.push(idx);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
let fHead = 0;
|
|
193
|
+
while (fHead < falloffQueue.length) {
|
|
194
|
+
const idx = falloffQueue[fHead] ?? -1;
|
|
195
|
+
fHead += 1;
|
|
196
|
+
const d = distanceFromForeground[idx] ?? -1;
|
|
197
|
+
if (d >= opts.falloffRadiusPx - 1) continue;
|
|
198
|
+
const x = idx % width;
|
|
199
|
+
const y = Math.floor(idx / width);
|
|
200
|
+
const neighbors = [];
|
|
201
|
+
if (x > 0) neighbors.push(idx - 1);
|
|
202
|
+
if (x < width - 1) neighbors.push(idx + 1);
|
|
203
|
+
if (y > 0) neighbors.push(idx - width);
|
|
204
|
+
if (y < height - 1) neighbors.push(idx + width);
|
|
205
|
+
for (const nIdx of neighbors) {
|
|
206
|
+
if (!isBackground[nIdx]) continue;
|
|
207
|
+
if ((distanceFromForeground[nIdx] ?? -1) !== -1) continue;
|
|
208
|
+
distanceFromForeground[nIdx] = d + 1;
|
|
209
|
+
falloffQueue.push(nIdx);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (let idx = 0; idx < pixelCount; idx++) {
|
|
213
|
+
if (!isBackground[idx]) continue;
|
|
214
|
+
const p = idx * 4;
|
|
215
|
+
const d = distanceFromForeground[idx] ?? -1;
|
|
216
|
+
const origAlpha = data[p + 3] ?? 0;
|
|
217
|
+
data[p + 3] = d === -1 ? 0 : Math.round(origAlpha * (opts.falloffRadiusPx - d) / (opts.falloffRadiusPx + 1));
|
|
218
|
+
}
|
|
219
|
+
ctx.putImageData(imageData, 0, 0);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// src/prepare-sprite.ts
|
|
223
|
+
var prepareSprite = (dataUrl, maxSide, options) => {
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
const img = new Image();
|
|
226
|
+
img.onload = () => {
|
|
227
|
+
const w = img.naturalWidth;
|
|
228
|
+
const h = img.naturalHeight;
|
|
229
|
+
if (w === 0 || h === 0) {
|
|
230
|
+
resolve(dataUrl);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const maxDim = Math.max(w, h);
|
|
234
|
+
const scale = maxDim > maxSide ? maxSide / maxDim : 1;
|
|
235
|
+
const newW = Math.max(1, Math.round(w * scale));
|
|
236
|
+
const newH = Math.max(1, Math.round(h * scale));
|
|
237
|
+
const side = Math.max(newW, newH);
|
|
238
|
+
const offsetX = Math.floor((side - newW) / 2);
|
|
239
|
+
const offsetY = Math.floor((side - newH) / 2);
|
|
240
|
+
const canvas = document.createElement("canvas");
|
|
241
|
+
canvas.width = side;
|
|
242
|
+
canvas.height = side;
|
|
243
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
244
|
+
if (!ctx) {
|
|
245
|
+
resolve(dataUrl);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
ctx.imageSmoothingEnabled = true;
|
|
249
|
+
ctx.imageSmoothingQuality = "high";
|
|
250
|
+
ctx.drawImage(img, offsetX, offsetY, newW, newH);
|
|
251
|
+
if (options?.stripBackground !== false) {
|
|
252
|
+
stripEdgeBackground(ctx, side, side, options);
|
|
253
|
+
}
|
|
254
|
+
resolve(canvas.toDataURL("image/png"));
|
|
255
|
+
};
|
|
256
|
+
img.onerror = () => reject(new Error("decode"));
|
|
257
|
+
img.src = dataUrl;
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
261
|
+
0 && (module.exports = {
|
|
262
|
+
prepareSprite,
|
|
263
|
+
stripEdgeBackground
|
|
264
|
+
});
|
|
265
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type StripBackgroundOptions = {
|
|
2
|
+
neutralChannelSpread?: number;
|
|
3
|
+
floodFillStepTolerance?: number;
|
|
4
|
+
floodFillMaxDrift?: number;
|
|
5
|
+
falloffRadiusPx?: number;
|
|
6
|
+
flatImageColorCoverage?: number;
|
|
7
|
+
flatImageColorQuantLevels?: number;
|
|
8
|
+
interiorBackgroundMatchTolerance?: number;
|
|
9
|
+
minStripAreaFraction?: number;
|
|
10
|
+
};
|
|
11
|
+
declare const stripEdgeBackground: (ctx: CanvasRenderingContext2D, width: number, height: number, options?: StripBackgroundOptions) => void;
|
|
12
|
+
|
|
13
|
+
type PrepareSpriteOptions = StripBackgroundOptions & {
|
|
14
|
+
stripBackground?: boolean;
|
|
15
|
+
};
|
|
16
|
+
declare const prepareSprite: (dataUrl: string, maxSide: number, options?: PrepareSpriteOptions) => Promise<string>;
|
|
17
|
+
|
|
18
|
+
export { type PrepareSpriteOptions, type StripBackgroundOptions, prepareSprite, stripEdgeBackground };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type StripBackgroundOptions = {
|
|
2
|
+
neutralChannelSpread?: number;
|
|
3
|
+
floodFillStepTolerance?: number;
|
|
4
|
+
floodFillMaxDrift?: number;
|
|
5
|
+
falloffRadiusPx?: number;
|
|
6
|
+
flatImageColorCoverage?: number;
|
|
7
|
+
flatImageColorQuantLevels?: number;
|
|
8
|
+
interiorBackgroundMatchTolerance?: number;
|
|
9
|
+
minStripAreaFraction?: number;
|
|
10
|
+
};
|
|
11
|
+
declare const stripEdgeBackground: (ctx: CanvasRenderingContext2D, width: number, height: number, options?: StripBackgroundOptions) => void;
|
|
12
|
+
|
|
13
|
+
type PrepareSpriteOptions = StripBackgroundOptions & {
|
|
14
|
+
stripBackground?: boolean;
|
|
15
|
+
};
|
|
16
|
+
declare const prepareSprite: (dataUrl: string, maxSide: number, options?: PrepareSpriteOptions) => Promise<string>;
|
|
17
|
+
|
|
18
|
+
export { type PrepareSpriteOptions, type StripBackgroundOptions, prepareSprite, stripEdgeBackground };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// src/strip-background.ts
|
|
2
|
+
var DEFAULT_OPTIONS = {
|
|
3
|
+
neutralChannelSpread: 20,
|
|
4
|
+
floodFillStepTolerance: 10,
|
|
5
|
+
floodFillMaxDrift: 24,
|
|
6
|
+
falloffRadiusPx: 3,
|
|
7
|
+
flatImageColorCoverage: 0.92,
|
|
8
|
+
flatImageColorQuantLevels: 32,
|
|
9
|
+
interiorBackgroundMatchTolerance: 16,
|
|
10
|
+
minStripAreaFraction: 0.03
|
|
11
|
+
};
|
|
12
|
+
var isNearNeutral = (r, g, b, spread) => {
|
|
13
|
+
const max = Math.max(r, g, b);
|
|
14
|
+
const min = Math.min(r, g, b);
|
|
15
|
+
return max - min <= spread;
|
|
16
|
+
};
|
|
17
|
+
var isFlatImage = (data, pixelCount, coverage, quantLevels) => {
|
|
18
|
+
const bucketCounts = /* @__PURE__ */ new Map();
|
|
19
|
+
let opaqueCount = 0;
|
|
20
|
+
for (let idx = 0; idx < pixelCount; idx++) {
|
|
21
|
+
const p = idx * 4;
|
|
22
|
+
if ((data[p + 3] ?? 0) === 0) continue;
|
|
23
|
+
opaqueCount += 1;
|
|
24
|
+
const rBucket = Math.floor((data[p] ?? 0) / 256 * quantLevels);
|
|
25
|
+
const gBucket = Math.floor((data[p + 1] ?? 0) / 256 * quantLevels);
|
|
26
|
+
const bBucket = Math.floor((data[p + 2] ?? 0) / 256 * quantLevels);
|
|
27
|
+
const key = (rBucket * quantLevels + gBucket) * quantLevels + bBucket;
|
|
28
|
+
bucketCounts.set(key, (bucketCounts.get(key) ?? 0) + 1);
|
|
29
|
+
}
|
|
30
|
+
if (opaqueCount === 0) return false;
|
|
31
|
+
let top1 = 0;
|
|
32
|
+
let top2 = 0;
|
|
33
|
+
for (const count of bucketCounts.values()) {
|
|
34
|
+
if (count > top1) {
|
|
35
|
+
top2 = top1;
|
|
36
|
+
top1 = count;
|
|
37
|
+
} else if (count > top2) {
|
|
38
|
+
top2 = count;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return (top1 + top2) / opaqueCount >= coverage;
|
|
42
|
+
};
|
|
43
|
+
var stripEdgeBackground = (ctx, width, height, options) => {
|
|
44
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
45
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
46
|
+
const { data } = imageData;
|
|
47
|
+
const pixelCount = width * height;
|
|
48
|
+
const isBackground = new Uint8Array(pixelCount);
|
|
49
|
+
const seeded = new Uint8Array(pixelCount);
|
|
50
|
+
const drift = new Uint16Array(pixelCount);
|
|
51
|
+
const queue = [];
|
|
52
|
+
const wasOriginallyOpaque = new Uint8Array(pixelCount);
|
|
53
|
+
let originalOpaqueCount = 0;
|
|
54
|
+
for (let idx = 0; idx < pixelCount; idx++) {
|
|
55
|
+
if ((data[idx * 4 + 3] ?? 0) === 0) continue;
|
|
56
|
+
wasOriginallyOpaque[idx] = 1;
|
|
57
|
+
originalOpaqueCount += 1;
|
|
58
|
+
}
|
|
59
|
+
let bgColorSumR = 0;
|
|
60
|
+
let bgColorSumG = 0;
|
|
61
|
+
let bgColorSumB = 0;
|
|
62
|
+
let bgColorSeedCount = 0;
|
|
63
|
+
const enqueueIfBackground = (idx) => {
|
|
64
|
+
if (seeded[idx]) return;
|
|
65
|
+
const p = idx * 4;
|
|
66
|
+
const r = data[p] ?? 0;
|
|
67
|
+
const g = data[p + 1] ?? 0;
|
|
68
|
+
const b = data[p + 2] ?? 0;
|
|
69
|
+
const isTransparent = (data[p + 3] ?? 0) === 0;
|
|
70
|
+
if (!isTransparent && !isNearNeutral(r, g, b, opts.neutralChannelSpread)) return;
|
|
71
|
+
seeded[idx] = 1;
|
|
72
|
+
isBackground[idx] = 1;
|
|
73
|
+
drift[idx] = 0;
|
|
74
|
+
if (!isTransparent) {
|
|
75
|
+
bgColorSumR += r;
|
|
76
|
+
bgColorSumG += g;
|
|
77
|
+
bgColorSumB += b;
|
|
78
|
+
bgColorSeedCount += 1;
|
|
79
|
+
}
|
|
80
|
+
queue.push(idx);
|
|
81
|
+
};
|
|
82
|
+
for (let x = 0; x < width; x++) {
|
|
83
|
+
enqueueIfBackground(x);
|
|
84
|
+
enqueueIfBackground((height - 1) * width + x);
|
|
85
|
+
}
|
|
86
|
+
for (let y = 0; y < height; y++) {
|
|
87
|
+
enqueueIfBackground(y * width);
|
|
88
|
+
enqueueIfBackground(y * width + (width - 1));
|
|
89
|
+
}
|
|
90
|
+
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
|
+
for (let idx = 0; idx < pixelCount; idx++) {
|
|
95
|
+
if (seeded[idx]) continue;
|
|
96
|
+
const p = idx * 4;
|
|
97
|
+
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;
|
|
103
|
+
seeded[idx] = 1;
|
|
104
|
+
isBackground[idx] = 1;
|
|
105
|
+
drift[idx] = 0;
|
|
106
|
+
queue.push(idx);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
let head = 0;
|
|
110
|
+
while (head < queue.length) {
|
|
111
|
+
const idx = queue[head] ?? -1;
|
|
112
|
+
head += 1;
|
|
113
|
+
const x = idx % width;
|
|
114
|
+
const y = Math.floor(idx / width);
|
|
115
|
+
const p = idx * 4;
|
|
116
|
+
const r0 = data[p] ?? 0;
|
|
117
|
+
const g0 = data[p + 1] ?? 0;
|
|
118
|
+
const b0 = data[p + 2] ?? 0;
|
|
119
|
+
const neighbors = [];
|
|
120
|
+
if (x > 0) neighbors.push(idx - 1);
|
|
121
|
+
if (x < width - 1) neighbors.push(idx + 1);
|
|
122
|
+
if (y > 0) neighbors.push(idx - width);
|
|
123
|
+
if (y < height - 1) neighbors.push(idx + width);
|
|
124
|
+
const currentDrift = drift[idx] ?? 0;
|
|
125
|
+
for (const nIdx of neighbors) {
|
|
126
|
+
if (seeded[nIdx]) continue;
|
|
127
|
+
const np = nIdx * 4;
|
|
128
|
+
if ((data[np + 3] ?? 0) === 0) {
|
|
129
|
+
seeded[nIdx] = 1;
|
|
130
|
+
isBackground[nIdx] = 1;
|
|
131
|
+
drift[nIdx] = currentDrift;
|
|
132
|
+
queue.push(nIdx);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const nr = data[np] ?? 0;
|
|
136
|
+
const ng = data[np + 1] ?? 0;
|
|
137
|
+
const nb = data[np + 2] ?? 0;
|
|
138
|
+
const step = Math.max(Math.abs(nr - r0), Math.abs(ng - g0), Math.abs(nb - b0));
|
|
139
|
+
if (step > opts.floodFillStepTolerance) continue;
|
|
140
|
+
const newDrift = currentDrift + step;
|
|
141
|
+
if (newDrift > opts.floodFillMaxDrift) continue;
|
|
142
|
+
seeded[nIdx] = 1;
|
|
143
|
+
isBackground[nIdx] = 1;
|
|
144
|
+
drift[nIdx] = newDrift;
|
|
145
|
+
queue.push(nIdx);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
let strippedContentCount = 0;
|
|
149
|
+
for (let idx = 0; idx < pixelCount; idx++) {
|
|
150
|
+
if (isBackground[idx] && wasOriginallyOpaque[idx]) strippedContentCount += 1;
|
|
151
|
+
}
|
|
152
|
+
if (originalOpaqueCount === 0 || strippedContentCount / originalOpaqueCount < opts.minStripAreaFraction) return;
|
|
153
|
+
const distanceFromForeground = new Int16Array(pixelCount).fill(-1);
|
|
154
|
+
const falloffQueue = [];
|
|
155
|
+
for (let idx = 0; idx < pixelCount; idx++) {
|
|
156
|
+
if (!isBackground[idx]) continue;
|
|
157
|
+
const x = idx % width;
|
|
158
|
+
const y = Math.floor(idx / width);
|
|
159
|
+
const touchesForeground = x > 0 && !isBackground[idx - 1] || x < width - 1 && !isBackground[idx + 1] || y > 0 && !isBackground[idx - width] || y < height - 1 && !isBackground[idx + width];
|
|
160
|
+
if (touchesForeground) {
|
|
161
|
+
distanceFromForeground[idx] = 0;
|
|
162
|
+
falloffQueue.push(idx);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
let fHead = 0;
|
|
166
|
+
while (fHead < falloffQueue.length) {
|
|
167
|
+
const idx = falloffQueue[fHead] ?? -1;
|
|
168
|
+
fHead += 1;
|
|
169
|
+
const d = distanceFromForeground[idx] ?? -1;
|
|
170
|
+
if (d >= opts.falloffRadiusPx - 1) continue;
|
|
171
|
+
const x = idx % width;
|
|
172
|
+
const y = Math.floor(idx / width);
|
|
173
|
+
const neighbors = [];
|
|
174
|
+
if (x > 0) neighbors.push(idx - 1);
|
|
175
|
+
if (x < width - 1) neighbors.push(idx + 1);
|
|
176
|
+
if (y > 0) neighbors.push(idx - width);
|
|
177
|
+
if (y < height - 1) neighbors.push(idx + width);
|
|
178
|
+
for (const nIdx of neighbors) {
|
|
179
|
+
if (!isBackground[nIdx]) continue;
|
|
180
|
+
if ((distanceFromForeground[nIdx] ?? -1) !== -1) continue;
|
|
181
|
+
distanceFromForeground[nIdx] = d + 1;
|
|
182
|
+
falloffQueue.push(nIdx);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (let idx = 0; idx < pixelCount; idx++) {
|
|
186
|
+
if (!isBackground[idx]) continue;
|
|
187
|
+
const p = idx * 4;
|
|
188
|
+
const d = distanceFromForeground[idx] ?? -1;
|
|
189
|
+
const origAlpha = data[p + 3] ?? 0;
|
|
190
|
+
data[p + 3] = d === -1 ? 0 : Math.round(origAlpha * (opts.falloffRadiusPx - d) / (opts.falloffRadiusPx + 1));
|
|
191
|
+
}
|
|
192
|
+
ctx.putImageData(imageData, 0, 0);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// src/prepare-sprite.ts
|
|
196
|
+
var prepareSprite = (dataUrl, maxSide, options) => {
|
|
197
|
+
return new Promise((resolve, reject) => {
|
|
198
|
+
const img = new Image();
|
|
199
|
+
img.onload = () => {
|
|
200
|
+
const w = img.naturalWidth;
|
|
201
|
+
const h = img.naturalHeight;
|
|
202
|
+
if (w === 0 || h === 0) {
|
|
203
|
+
resolve(dataUrl);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const maxDim = Math.max(w, h);
|
|
207
|
+
const scale = maxDim > maxSide ? maxSide / maxDim : 1;
|
|
208
|
+
const newW = Math.max(1, Math.round(w * scale));
|
|
209
|
+
const newH = Math.max(1, Math.round(h * scale));
|
|
210
|
+
const side = Math.max(newW, newH);
|
|
211
|
+
const offsetX = Math.floor((side - newW) / 2);
|
|
212
|
+
const offsetY = Math.floor((side - newH) / 2);
|
|
213
|
+
const canvas = document.createElement("canvas");
|
|
214
|
+
canvas.width = side;
|
|
215
|
+
canvas.height = side;
|
|
216
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
217
|
+
if (!ctx) {
|
|
218
|
+
resolve(dataUrl);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
ctx.imageSmoothingEnabled = true;
|
|
222
|
+
ctx.imageSmoothingQuality = "high";
|
|
223
|
+
ctx.drawImage(img, offsetX, offsetY, newW, newH);
|
|
224
|
+
if (options?.stripBackground !== false) {
|
|
225
|
+
stripEdgeBackground(ctx, side, side, options);
|
|
226
|
+
}
|
|
227
|
+
resolve(canvas.toDataURL("image/png"));
|
|
228
|
+
};
|
|
229
|
+
img.onerror = () => reject(new Error("decode"));
|
|
230
|
+
img.src = dataUrl;
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
export {
|
|
234
|
+
prepareSprite,
|
|
235
|
+
stripEdgeBackground
|
|
236
|
+
};
|
|
237
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sprite-strip",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Edge-seeded flood-fill background removal and square-pad resize for sprite images, via browser Canvas 2D.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"lint": "eslint src test --fix",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"prepublishOnly": "yarn build"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"canvas",
|
|
29
|
+
"sprite",
|
|
30
|
+
"background-removal",
|
|
31
|
+
"flood-fill",
|
|
32
|
+
"image-processing"
|
|
33
|
+
],
|
|
34
|
+
"author": "Cody Douglass",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/c0d3ster/sprite-strip.git"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
|
42
|
+
"@typescript-eslint/parser": "^8.18.0",
|
|
43
|
+
"eslint": "^9.17.0",
|
|
44
|
+
"eslint-plugin-prefer-arrow": "^1.2.3",
|
|
45
|
+
"tsup": "^8.3.5",
|
|
46
|
+
"typescript": "^5.7.2",
|
|
47
|
+
"vitest": "^2.1.8"
|
|
48
|
+
}
|
|
49
|
+
}
|