203 lines
8.6 KiB
JavaScript
203 lines
8.6 KiB
JavaScript
|
|
import { Skia, AlphaType, ColorType, } from '@shopify/react-native-skia';
|
|||
|
|
/**
|
|||
|
|
* Separable box blur on premultiplied RGBA (4-channel 0-255).
|
|||
|
|
* Feather radius in pixels (at the working pick buffer resolution).
|
|||
|
|
* Works on premultiplied values so that color naturally diffuses alongside alpha
|
|||
|
|
* into the feather region — preventing "scatter dots" at resolution-mismatched boundaries.
|
|||
|
|
*/
|
|||
|
|
function boxBlurRgbaPremul(rgba, cols, rows, radius) {
|
|||
|
|
const r = Math.max(1, Math.min(16, Math.round(radius)));
|
|||
|
|
const tmp = new Uint8Array(cols * rows * 4);
|
|||
|
|
const dst = new Uint8Array(cols * rows * 4);
|
|||
|
|
// Horizontal pass
|
|||
|
|
for (let y = 0; y < rows; y++) {
|
|||
|
|
for (let x = 0; x < cols; x++) {
|
|||
|
|
let sumR = 0, sumG = 0, sumB = 0, sumA = 0;
|
|||
|
|
let cnt = 0;
|
|||
|
|
const base = y * cols * 4;
|
|||
|
|
for (let dx = -r; dx <= r; dx++) {
|
|||
|
|
const xx = x + dx;
|
|||
|
|
if (xx >= 0 && xx < cols) {
|
|||
|
|
const idx = base + xx * 4;
|
|||
|
|
sumR += rgba[idx];
|
|||
|
|
sumG += rgba[idx + 1];
|
|||
|
|
sumB += rgba[idx + 2];
|
|||
|
|
sumA += rgba[idx + 3];
|
|||
|
|
cnt++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const o = base + x * 4;
|
|||
|
|
tmp[o] = Math.round(sumR / cnt);
|
|||
|
|
tmp[o + 1] = Math.round(sumG / cnt);
|
|||
|
|
tmp[o + 2] = Math.round(sumB / cnt);
|
|||
|
|
tmp[o + 3] = Math.round(sumA / cnt);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Vertical pass
|
|||
|
|
for (let x = 0; x < cols; x++) {
|
|||
|
|
for (let y = 0; y < rows; y++) {
|
|||
|
|
let sumR = 0, sumG = 0, sumB = 0, sumA = 0;
|
|||
|
|
let cnt = 0;
|
|||
|
|
for (let dy = -r; dy <= r; dy++) {
|
|||
|
|
const yy = y + dy;
|
|||
|
|
if (yy >= 0 && yy < rows) {
|
|||
|
|
const idx = yy * cols * 4 + x * 4;
|
|||
|
|
sumR += tmp[idx];
|
|||
|
|
sumG += tmp[idx + 1];
|
|||
|
|
sumB += tmp[idx + 2];
|
|||
|
|
sumA += tmp[idx + 3];
|
|||
|
|
cnt++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const o = y * cols * 4 + x * 4;
|
|||
|
|
dst[o] = Math.round(sumR / cnt);
|
|||
|
|
dst[o + 1] = Math.round(sumG / cnt);
|
|||
|
|
dst[o + 2] = Math.round(sumB / cnt);
|
|||
|
|
dst[o + 3] = Math.round(sumA / cnt);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return dst;
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* Separable dilation (max filter) on premultiplied RGBA.
|
|||
|
|
* Fills small holes (alpha=0) inside painted regions by taking the
|
|||
|
|
* per-channel maximum in the neighborhood. Applied after boxBlur to
|
|||
|
|
* eliminate scatter dots caused by segmentation gaps in the pick buffer.
|
|||
|
|
*/
|
|||
|
|
function dilateRgbaPremul(rgba, cols, rows, radius) {
|
|||
|
|
const r = Math.max(1, Math.min(8, Math.round(radius)));
|
|||
|
|
const tmp = new Uint8Array(cols * rows * 4);
|
|||
|
|
const dst = new Uint8Array(cols * rows * 4);
|
|||
|
|
// Horizontal pass
|
|||
|
|
for (let y = 0; y < rows; y++) {
|
|||
|
|
for (let x = 0; x < cols; x++) {
|
|||
|
|
let maxR = 0, maxG = 0, maxB = 0, maxA = 0;
|
|||
|
|
const base = y * cols * 4;
|
|||
|
|
for (let dx = -r; dx <= r; dx++) {
|
|||
|
|
const xx = x + dx;
|
|||
|
|
if (xx >= 0 && xx < cols) {
|
|||
|
|
const idx = base + xx * 4;
|
|||
|
|
if (rgba[idx + 3] > maxA) {
|
|||
|
|
maxR = rgba[idx];
|
|||
|
|
maxG = rgba[idx + 1];
|
|||
|
|
maxB = rgba[idx + 2];
|
|||
|
|
maxA = rgba[idx + 3];
|
|||
|
|
}
|
|||
|
|
else if (rgba[idx + 3] === maxA && maxA > 0) {
|
|||
|
|
maxR = Math.max(maxR, rgba[idx]);
|
|||
|
|
maxG = Math.max(maxG, rgba[idx + 1]);
|
|||
|
|
maxB = Math.max(maxB, rgba[idx + 2]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const o = base + x * 4;
|
|||
|
|
tmp[o] = maxR;
|
|||
|
|
tmp[o + 1] = maxG;
|
|||
|
|
tmp[o + 2] = maxB;
|
|||
|
|
tmp[o + 3] = maxA;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Vertical pass
|
|||
|
|
for (let x = 0; x < cols; x++) {
|
|||
|
|
for (let y = 0; y < rows; y++) {
|
|||
|
|
let maxR = 0, maxG = 0, maxB = 0, maxA = 0;
|
|||
|
|
for (let dy = -r; dy <= r; dy++) {
|
|||
|
|
const yy = y + dy;
|
|||
|
|
if (yy >= 0 && yy < rows) {
|
|||
|
|
const idx = yy * cols * 4 + x * 4;
|
|||
|
|
if (tmp[idx + 3] > maxA) {
|
|||
|
|
maxR = tmp[idx];
|
|||
|
|
maxG = tmp[idx + 1];
|
|||
|
|
maxB = tmp[idx + 2];
|
|||
|
|
maxA = tmp[idx + 3];
|
|||
|
|
}
|
|||
|
|
else if (tmp[idx + 3] === maxA && maxA > 0) {
|
|||
|
|
maxR = Math.max(maxR, tmp[idx]);
|
|||
|
|
maxG = Math.max(maxG, tmp[idx + 1]);
|
|||
|
|
maxB = Math.max(maxB, tmp[idx + 2]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const o = y * cols * 4 + x * 4;
|
|||
|
|
dst[o] = maxR;
|
|||
|
|
dst[o + 1] = maxG;
|
|||
|
|
dst[o + 2] = maxB;
|
|||
|
|
dst[o + 3] = maxA;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return dst;
|
|||
|
|
}
|
|||
|
|
/** 按 pickMap 展开的上色颜色图(与 pick 同尺寸,未上色像素 a=0)。支持 maskFeather 产生软边缘 alpha。 */
|
|||
|
|
export function buildPaintColorMapImage(pickBuffer, cols, rows, paintedRegions, featherRadius = 0) {
|
|||
|
|
const pixelCount = cols * rows;
|
|||
|
|
const colorByPickCode = new Map();
|
|||
|
|
for (const [regionId, color] of paintedRegions) {
|
|||
|
|
colorByPickCode.set(regionId + 1, color);
|
|||
|
|
}
|
|||
|
|
// ── Perf fast-path: no feather → 1-pass direct unpremul RGBA (O(N)) ──
|
|||
|
|
// Skips premul buffer, boxBlur H+V, dilate H+V, and unpremul conversion.
|
|||
|
|
// The GPU shader handles boundary noise via unpremultiply (rgb /= a) +
|
|||
|
|
// smoothstep gate, so no CPU-side dilate/filter is needed.
|
|||
|
|
if (featherRadius <= 0.1 && featherRadius >= -0.1) {
|
|||
|
|
const rgba = new Uint8Array(pixelCount * 4);
|
|||
|
|
for (let i = 0; i < pixelCount; i++) {
|
|||
|
|
const code = pickBuffer[i];
|
|||
|
|
const color = code > 0 ? colorByPickCode.get(code) : undefined;
|
|||
|
|
const o = i * 4;
|
|||
|
|
if (color) {
|
|||
|
|
rgba[o] = color.r;
|
|||
|
|
rgba[o + 1] = color.g;
|
|||
|
|
rgba[o + 2] = color.b;
|
|||
|
|
rgba[o + 3] = 255;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const data = Skia.Data.fromBytes(rgba);
|
|||
|
|
return Skia.Image.MakeImage({ width: cols, height: rows, alphaType: AlphaType.Unpremul, colorType: ColorType.RGBA_8888 }, data, cols * 4);
|
|||
|
|
}
|
|||
|
|
// ── Feathered path: premul + boxBlur + dilate + unpremul ──
|
|||
|
|
// Build hard premultiplied RGBA: where a pick code maps to a painted color,
|
|||
|
|
// store (R, G, B, 255) in premul space. Unpainted pixels stay (0,0,0,0).
|
|||
|
|
const hardPremul = new Uint8Array(pixelCount * 4);
|
|||
|
|
for (let i = 0; i < pixelCount; i++) {
|
|||
|
|
const code = pickBuffer[i];
|
|||
|
|
const color = code > 0 ? colorByPickCode.get(code) : undefined;
|
|||
|
|
const o = i * 4;
|
|||
|
|
if (color) {
|
|||
|
|
hardPremul[o] = color.r;
|
|||
|
|
hardPremul[o + 1] = color.g;
|
|||
|
|
hardPremul[o + 2] = color.b;
|
|||
|
|
hardPremul[o + 3] = 255;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Box-blur premultiplied RGBA so color naturally diffuses alongside alpha
|
|||
|
|
// into the feather region. This prevents scatter dots from bilinear
|
|||
|
|
// interpolation at resolution-mismatched boundaries (where unpainted=black
|
|||
|
|
// would otherwise leak into boundary samples).
|
|||
|
|
const blurredPremul = boxBlurRgbaPremul(hardPremul, cols, rows, featherRadius);
|
|||
|
|
// Dilation pass: fill small holes (alpha=0) inside painted regions that
|
|||
|
|
// survive the box blur due to segmentation gaps in the pick buffer.
|
|||
|
|
const filledPremul = dilateRgbaPremul(blurredPremul, cols, rows, featherRadius);
|
|||
|
|
// Unpremultiply and store as RGBA for the final SkImage.
|
|||
|
|
const rgba = new Uint8Array(pixelCount * 4);
|
|||
|
|
for (let i = 0; i < pixelCount; i++) {
|
|||
|
|
const o = i * 4;
|
|||
|
|
const a = filledPremul[o + 3];
|
|||
|
|
if (a > 0) {
|
|||
|
|
rgba[o] = Math.min(255, Math.round((filledPremul[o] * 255) / a));
|
|||
|
|
rgba[o + 1] = Math.min(255, Math.round((filledPremul[o + 1] * 255) / a));
|
|||
|
|
rgba[o + 2] = Math.min(255, Math.round((filledPremul[o + 2] * 255) / a));
|
|||
|
|
rgba[o + 3] = a;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const data = Skia.Data.fromBytes(rgba);
|
|||
|
|
return Skia.Image.MakeImage({
|
|||
|
|
width: cols,
|
|||
|
|
height: rows,
|
|||
|
|
// Unpremul: rgb stores the target paint color (unmodulated), alpha stores coverage (soft edge feather).
|
|||
|
|
// This ensures ImageShader sampling returns the intended paint color + separate coverage alpha,
|
|||
|
|
// without premultiplication assumptions that could cause dark fringes on soft edges.
|
|||
|
|
alphaType: AlphaType.Unpremul,
|
|||
|
|
colorType: ColorType.RGBA_8888,
|
|||
|
|
}, data, cols * 4);
|
|||
|
|
}
|
|||
|
|
//# sourceMappingURL=paintColorMapTexture.js.map
|