129 lines
3.8 KiB
TypeScript
129 lines
3.8 KiB
TypeScript
import { parseHex } from './alpha';
|
|
import { ToolError } from './errors';
|
|
import { clonePixelImage, createPixelImage, type PixelImage } from './types';
|
|
import { sampleBilinear } from './geometry';
|
|
|
|
export type AffineMatrix = [number, number, number, number, number, number];
|
|
|
|
export function invertAffine([a, b, c, d, e, f]: AffineMatrix): AffineMatrix {
|
|
const det = a * d - b * c;
|
|
if (Math.abs(det) < 1e-12) {
|
|
throw new ToolError('errors.badTransform');
|
|
}
|
|
const ia = d / det;
|
|
const ib = -b / det;
|
|
const ic = -c / det;
|
|
const id = a / det;
|
|
return [ia, ib, ic, id, -(ia * e + ic * f), -(ib * e + id * f)];
|
|
}
|
|
|
|
const EPS = 1e-9;
|
|
|
|
export function transformImage(
|
|
img: PixelImage,
|
|
dstToSrc: AffineMatrix,
|
|
outWidth: number,
|
|
outHeight: number,
|
|
bgHex?: string
|
|
): PixelImage {
|
|
const [a, b, c, d, e, f] = dstToSrc;
|
|
const out = createPixelImage(outWidth, outHeight);
|
|
const bg = bgHex ? parseHex(bgHex) : null;
|
|
for (let y = 0; y < outHeight; y++) {
|
|
for (let x = 0; x < outWidth; x++) {
|
|
const sx = a * x + c * y + e;
|
|
const sy = b * x + d * y + f;
|
|
const di = (y * outWidth + x) * 4;
|
|
if (sx < -EPS || sy < -EPS || sx > img.width - 1 + EPS || sy > img.height - 1 + EPS) {
|
|
if (bg) {
|
|
out.data[di] = bg[0];
|
|
out.data[di + 1] = bg[1];
|
|
out.data[di + 2] = bg[2];
|
|
out.data[di + 3] = 255;
|
|
}
|
|
continue;
|
|
}
|
|
const [r, g, bl, al] = sampleBilinear(img, sx, sy);
|
|
if (bg) {
|
|
const sa = al / 255;
|
|
out.data[di] = r * sa + bg[0] * (1 - sa);
|
|
out.data[di + 1] = g * sa + bg[1] * (1 - sa);
|
|
out.data[di + 2] = bl * sa + bg[2] * (1 - sa);
|
|
out.data[di + 3] = 255;
|
|
} else {
|
|
out.data[di] = r;
|
|
out.data[di + 1] = g;
|
|
out.data[di + 2] = bl;
|
|
out.data[di + 3] = al;
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function centeredTransform(img: PixelImage, forward: AffineMatrix): PixelImage {
|
|
const inv = invertAffine(forward);
|
|
|
|
let minX = Infinity;
|
|
let maxX = -Infinity;
|
|
let minY = Infinity;
|
|
let maxY = -Infinity;
|
|
for (const [px, py] of [
|
|
[0.5, 0.5],
|
|
[img.width - 0.5, 0.5],
|
|
[0.5, img.height - 0.5],
|
|
[img.width - 0.5, img.height - 0.5]
|
|
]) {
|
|
const qx = forward[0] * px + forward[2] * py + forward[4];
|
|
const qy = forward[1] * px + forward[3] * py + forward[5];
|
|
minX = Math.min(minX, qx);
|
|
maxX = Math.max(maxX, qx);
|
|
minY = Math.min(minY, qy);
|
|
maxY = Math.max(maxY, qy);
|
|
}
|
|
const outW = Math.round(maxX - minX) + 1;
|
|
const outH = Math.round(maxY - minY) + 1;
|
|
const tx = inv[0] * minX + inv[2] * minY - 0.5;
|
|
const ty = inv[1] * minX + inv[3] * minY - 0.5;
|
|
return transformImage(img, [inv[0], inv[1], inv[2], inv[3], tx, ty], outW, outH);
|
|
}
|
|
|
|
export function skewImage(img: PixelImage, degX: number, degY: number): PixelImage {
|
|
const kx = Math.tan((degX * Math.PI) / 180);
|
|
const ky = Math.tan((degY * Math.PI) / 180);
|
|
if (!Number.isFinite(kx) || !Number.isFinite(ky)) {
|
|
throw new ToolError('errors.skewAngle');
|
|
}
|
|
return centeredTransform(img, [1, ky, kx, 1, 0, 0]);
|
|
}
|
|
|
|
export function rotateFreeImage(img: PixelImage, degrees: number): PixelImage {
|
|
const rad = (degrees * Math.PI) / 180;
|
|
let cos = Math.cos(rad);
|
|
let sin = Math.sin(rad);
|
|
if (Math.abs(sin) < 1e-12) sin = 0;
|
|
if (Math.abs(cos) < 1e-12) cos = 0;
|
|
return centeredTransform(img, [cos, sin, -sin, cos, 0, 0]);
|
|
}
|
|
|
|
export function zoomImage(img: PixelImage, scalePercent: number): PixelImage {
|
|
const scale = Math.min(Math.max(scalePercent, 100), 1000) / 100;
|
|
if (scale === 1) return clonePixelImage(img);
|
|
const cx = (img.width - 1) / 2;
|
|
const cy = (img.height - 1) / 2;
|
|
const out = createPixelImage(img.width, img.height);
|
|
for (let y = 0; y < out.height; y++) {
|
|
for (let x = 0; x < out.width; x++) {
|
|
const sx = cx + (x - cx) / scale;
|
|
const sy = cy + (y - cy) / scale;
|
|
const [r, g, b, a] = sampleBilinear(img, sx, sy);
|
|
const di = (y * out.width + x) * 4;
|
|
out.data[di] = r;
|
|
out.data[di + 1] = g;
|
|
out.data[di + 2] = b;
|
|
out.data[di + 3] = a;
|
|
}
|
|
}
|
|
return out;
|
|
}
|