Fix: background splitting bug
This commit is contained in:
@@ -33,6 +33,22 @@ export type UiSnapshot = {
|
||||
};
|
||||
|
||||
type AnimationMap = Map<string, UiAnimationClip>;
|
||||
const KEY_R = 0xff;
|
||||
const KEY_G = 0x00;
|
||||
const KEY_B = 0xff;
|
||||
const FALLBACK_MIN_CONNECTED_RATIO = 0.005;
|
||||
const CONNECTED_HUE_MIN = 270;
|
||||
const CONNECTED_HUE_MAX = 350;
|
||||
const CONNECTED_SAT_MIN = 0.25;
|
||||
const CONNECTED_VAL_MIN = 0.08;
|
||||
const FALLBACK_HUE_MIN = 255;
|
||||
const FALLBACK_HUE_MAX = 355;
|
||||
const FALLBACK_SAT_MIN = 0.15;
|
||||
const FALLBACK_VAL_MIN = 0.04;
|
||||
const HALO_HUE_MIN = 245;
|
||||
const HALO_HUE_MAX = 355;
|
||||
const HALO_SAT_MIN = 0.15;
|
||||
const HALO_VAL_MIN = 0.04;
|
||||
|
||||
export class PixiPetRenderer {
|
||||
private app: Application;
|
||||
@@ -101,28 +117,144 @@ export class PixiPetRenderer {
|
||||
ctx.drawImage(image, 0, 0);
|
||||
const frame = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = frame.data;
|
||||
const keyR = 0xff;
|
||||
const keyG = 0x00;
|
||||
const keyB = 0xff;
|
||||
const hardTolerance = 22;
|
||||
const softTolerance = 46;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const dr = Math.abs(data[i] - keyR);
|
||||
const dg = Math.abs(data[i + 1] - keyG);
|
||||
const db = Math.abs(data[i + 2] - keyB);
|
||||
const maxDistance = Math.max(dr, dg, db);
|
||||
if (maxDistance <= hardTolerance) {
|
||||
data[i + 3] = 0;
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
const pixelCount = width * height;
|
||||
const isKeyLike = new Uint8Array(pixelCount);
|
||||
const removedBg = new Uint8Array(pixelCount);
|
||||
const queue = new Int32Array(pixelCount);
|
||||
let head = 0;
|
||||
let tail = 0;
|
||||
|
||||
const indexFor = (x: number, y: number): number => y * width + x;
|
||||
const channelOffset = (index: number): number => index * 4;
|
||||
const enqueueIfKeyLike = (x: number, y: number): void => {
|
||||
const idx = indexFor(x, y);
|
||||
if (isKeyLike[idx] === 1 && removedBg[idx] === 0) {
|
||||
removedBg[idx] = 1;
|
||||
queue[tail] = idx;
|
||||
tail += 1;
|
||||
}
|
||||
};
|
||||
|
||||
for (let idx = 0; idx < pixelCount; idx += 1) {
|
||||
const offset = channelOffset(idx);
|
||||
const [h, s, v] = PixiPetRenderer.rgbToHsv(
|
||||
data[offset],
|
||||
data[offset + 1],
|
||||
data[offset + 2]
|
||||
);
|
||||
if (
|
||||
PixiPetRenderer.isHueInRange(h, CONNECTED_HUE_MIN, CONNECTED_HUE_MAX) &&
|
||||
s >= CONNECTED_SAT_MIN &&
|
||||
v >= CONNECTED_VAL_MIN
|
||||
) {
|
||||
isKeyLike[idx] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (let x = 0; x < width; x += 1) {
|
||||
enqueueIfKeyLike(x, 0);
|
||||
enqueueIfKeyLike(x, height - 1);
|
||||
}
|
||||
for (let y = 1; y < height - 1; y += 1) {
|
||||
enqueueIfKeyLike(0, y);
|
||||
enqueueIfKeyLike(width - 1, y);
|
||||
}
|
||||
|
||||
while (head < tail) {
|
||||
const idx = queue[head];
|
||||
head += 1;
|
||||
const x = idx % width;
|
||||
const y = Math.floor(idx / width);
|
||||
if (x > 0) {
|
||||
enqueueIfKeyLike(x - 1, y);
|
||||
}
|
||||
if (x + 1 < width) {
|
||||
enqueueIfKeyLike(x + 1, y);
|
||||
}
|
||||
if (y > 0) {
|
||||
enqueueIfKeyLike(x, y - 1);
|
||||
}
|
||||
if (y + 1 < height) {
|
||||
enqueueIfKeyLike(x, y + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const connectedRemovedCount = tail;
|
||||
for (let idx = 0; idx < pixelCount; idx += 1) {
|
||||
if (removedBg[idx] !== 1) {
|
||||
continue;
|
||||
}
|
||||
if (maxDistance <= softTolerance) {
|
||||
const alphaScale =
|
||||
(maxDistance - hardTolerance) / (softTolerance - hardTolerance);
|
||||
const suppress = 1 - alphaScale;
|
||||
data[i + 3] = Math.round(data[i + 3] * alphaScale);
|
||||
// Remove magenta spill from antialiased edges after alpha reduction.
|
||||
data[i] = Math.round(data[i] * (1 - 0.4 * suppress));
|
||||
data[i + 2] = Math.round(data[i + 2] * (1 - 0.4 * suppress));
|
||||
const offset = channelOffset(idx);
|
||||
data[offset + 3] = 0;
|
||||
}
|
||||
|
||||
const needsFallback =
|
||||
connectedRemovedCount / Math.max(pixelCount, 1) < FALLBACK_MIN_CONNECTED_RATIO;
|
||||
if (needsFallback) {
|
||||
for (let idx = 0; idx < pixelCount; idx += 1) {
|
||||
const offset = channelOffset(idx);
|
||||
const [h, s, v] = PixiPetRenderer.rgbToHsv(
|
||||
data[offset],
|
||||
data[offset + 1],
|
||||
data[offset + 2]
|
||||
);
|
||||
const maxDistanceFromHardKey = PixiPetRenderer.maxColorDistance(
|
||||
data[offset],
|
||||
data[offset + 1],
|
||||
data[offset + 2],
|
||||
KEY_R,
|
||||
KEY_G,
|
||||
KEY_B
|
||||
);
|
||||
if (
|
||||
(PixiPetRenderer.isHueInRange(h, FALLBACK_HUE_MIN, FALLBACK_HUE_MAX) &&
|
||||
s >= FALLBACK_SAT_MIN &&
|
||||
v >= FALLBACK_VAL_MIN) ||
|
||||
maxDistanceFromHardKey <= 96
|
||||
) {
|
||||
data[offset + 3] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let y = 0; y < height; y += 1) {
|
||||
for (let x = 0; x < width; x += 1) {
|
||||
const idx = indexFor(x, y);
|
||||
if (data[channelOffset(idx) + 3] === 0) {
|
||||
continue;
|
||||
}
|
||||
let touchesBackground = false;
|
||||
if (x > 0 && data[channelOffset(indexFor(x - 1, y)) + 3] === 0) {
|
||||
touchesBackground = true;
|
||||
} else if (x + 1 < width && data[channelOffset(indexFor(x + 1, y)) + 3] === 0) {
|
||||
touchesBackground = true;
|
||||
} else if (y > 0 && data[channelOffset(indexFor(x, y - 1)) + 3] === 0) {
|
||||
touchesBackground = true;
|
||||
} else if (y + 1 < height && data[channelOffset(indexFor(x, y + 1)) + 3] === 0) {
|
||||
touchesBackground = true;
|
||||
}
|
||||
if (!touchesBackground) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const offset = channelOffset(idx);
|
||||
const [h, s, v] = PixiPetRenderer.rgbToHsv(
|
||||
data[offset],
|
||||
data[offset + 1],
|
||||
data[offset + 2]
|
||||
);
|
||||
if (
|
||||
!PixiPetRenderer.isHueInRange(h, HALO_HUE_MIN, HALO_HUE_MAX) ||
|
||||
s < HALO_SAT_MIN ||
|
||||
v < HALO_VAL_MIN
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
data[offset] = Math.round(data[offset] * 0.72);
|
||||
data[offset + 2] = Math.round(data[offset + 2] * 0.72);
|
||||
data[offset + 3] = Math.round(data[offset + 3] * 0.86);
|
||||
}
|
||||
}
|
||||
ctx.putImageData(frame, 0, 0);
|
||||
@@ -135,6 +267,53 @@ export class PixiPetRenderer {
|
||||
});
|
||||
}
|
||||
|
||||
private static maxColorDistance(
|
||||
r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
keyR: number,
|
||||
keyG: number,
|
||||
keyB: number
|
||||
): number {
|
||||
const dr = Math.abs(r - keyR);
|
||||
const dg = Math.abs(g - keyG);
|
||||
const db = Math.abs(b - keyB);
|
||||
return Math.max(dr, dg, db);
|
||||
}
|
||||
|
||||
private static rgbToHsv(r: number, g: number, b: number): [number, number, number] {
|
||||
const rf = r / 255;
|
||||
const gf = g / 255;
|
||||
const bf = b / 255;
|
||||
const max = Math.max(rf, gf, bf);
|
||||
const min = Math.min(rf, gf, bf);
|
||||
const delta = max - min;
|
||||
|
||||
let hue = 0;
|
||||
if (delta > 0) {
|
||||
if (max === rf) {
|
||||
hue = 60 * (((gf - bf) / delta) % 6);
|
||||
} else if (max === gf) {
|
||||
hue = 60 * ((bf - rf) / delta + 2);
|
||||
} else {
|
||||
hue = 60 * ((rf - gf) / delta + 4);
|
||||
}
|
||||
}
|
||||
if (hue < 0) {
|
||||
hue += 360;
|
||||
}
|
||||
const saturation = max === 0 ? 0 : delta / max;
|
||||
const value = max;
|
||||
return [hue, saturation, value];
|
||||
}
|
||||
|
||||
private static isHueInRange(hue: number, min: number, max: number): boolean {
|
||||
if (min <= max) {
|
||||
return hue >= min && hue <= max;
|
||||
}
|
||||
return hue >= min || hue <= max;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.disposed) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user