Fix: background splitting bug
This commit is contained in:
@@ -2,7 +2,13 @@ import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { LogicalPosition, LogicalSize, getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import {
|
||||
PhysicalPosition,
|
||||
PhysicalSize,
|
||||
currentMonitor,
|
||||
getCurrentWindow,
|
||||
monitorFromPoint
|
||||
} from "@tauri-apps/api/window";
|
||||
import { PixiPetRenderer, type UiSpritePack, type UiSnapshot } from "./renderer/pixi_pet";
|
||||
import "./styles.css";
|
||||
|
||||
@@ -45,29 +51,58 @@ function fittedWindowSize(
|
||||
scale: number
|
||||
): { width: number; height: number } {
|
||||
const safeScale = Number.isFinite(scale) && scale > 0 ? scale : 1;
|
||||
const width = Math.max(frameWidth * safeScale + WINDOW_PADDING, MIN_WINDOW_SIZE);
|
||||
const height = Math.max(frameHeight * safeScale + WINDOW_PADDING, MIN_WINDOW_SIZE);
|
||||
const width = Math.round(Math.max(frameWidth * safeScale + WINDOW_PADDING, MIN_WINDOW_SIZE));
|
||||
const height = Math.round(Math.max(frameHeight * safeScale + WINDOW_PADDING, MIN_WINDOW_SIZE));
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
async function fitWindowForScale(pack: UiSpritePack, scale: number): Promise<void> {
|
||||
const window = getCurrentWindow();
|
||||
const [outerPosition, innerSize] = await Promise.all([window.outerPosition(), window.innerSize()]);
|
||||
|
||||
const target = fittedWindowSize(pack.frame_width, pack.frame_height, scale);
|
||||
const widthChanged = Math.abs(target.width - innerSize.width) > SIZE_EPSILON;
|
||||
const heightChanged = Math.abs(target.height - innerSize.height) > SIZE_EPSILON;
|
||||
const centerX = outerPosition.x + innerSize.width / 2;
|
||||
const centerY = outerPosition.y + innerSize.height / 2;
|
||||
let targetWidth = target.width;
|
||||
let targetHeight = target.height;
|
||||
let targetX = centerX - targetWidth / 2;
|
||||
let targetY = centerY - targetHeight / 2;
|
||||
let monitor:
|
||||
| {
|
||||
position: { x: number; y: number };
|
||||
size: { width: number; height: number };
|
||||
workArea: { position: { x: number; y: number }; size: { width: number; height: number } };
|
||||
}
|
||||
| null = null;
|
||||
|
||||
try {
|
||||
monitor = (await monitorFromPoint(centerX, centerY)) ?? (await currentMonitor());
|
||||
} catch {
|
||||
monitor = null;
|
||||
}
|
||||
|
||||
if (monitor !== null) {
|
||||
targetWidth = Math.min(targetWidth, monitor.workArea.size.width);
|
||||
targetHeight = Math.min(targetHeight, monitor.workArea.size.height);
|
||||
targetX = centerX - targetWidth / 2;
|
||||
targetY = centerY - targetHeight / 2;
|
||||
}
|
||||
|
||||
const widthChanged = Math.abs(targetWidth - innerSize.width) > SIZE_EPSILON;
|
||||
const heightChanged = Math.abs(targetHeight - innerSize.height) > SIZE_EPSILON;
|
||||
if (!widthChanged && !heightChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaWidth = target.width - innerSize.width;
|
||||
const deltaHeight = target.height - innerSize.height;
|
||||
const targetX = outerPosition.x - deltaWidth / 2;
|
||||
const targetY = outerPosition.y - deltaHeight;
|
||||
|
||||
await window.setSize(new LogicalSize(target.width, target.height));
|
||||
await window.setPosition(new LogicalPosition(targetX, targetY));
|
||||
await window.setSize(new PhysicalSize(targetWidth, targetHeight));
|
||||
if (monitor !== null) {
|
||||
const minX = Math.round(monitor.workArea.position.x);
|
||||
const minY = Math.round(monitor.workArea.position.y);
|
||||
const maxX = Math.round(monitor.workArea.position.x + monitor.workArea.size.width - targetWidth);
|
||||
const maxY = Math.round(monitor.workArea.position.y + monitor.workArea.size.height - targetHeight);
|
||||
targetX = maxX < minX ? minX : Math.min(Math.max(targetX, minX), maxX);
|
||||
targetY = maxY < minY ? minY : Math.min(Math.max(targetY, minY), maxY);
|
||||
}
|
||||
await window.setPosition(new PhysicalPosition(Math.round(targetX), Math.round(targetY)));
|
||||
}
|
||||
|
||||
function MainOverlayWindow(): JSX.Element {
|
||||
|
||||
@@ -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