feat: add core tools - transfor, light, effects
This commit is contained in:
@@ -0,0 +1,89 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { rotate90, sampleBilinear } from './geometry';
|
||||||
|
import { rotateFreeImage, skewImage, transformImage, zoomImage } from './affine';
|
||||||
|
import { makeImage } from './test-helpers';
|
||||||
|
|
||||||
|
describe('sampleBilinear', () => {
|
||||||
|
it('целые координаты возвращают точный пиксель', () => {
|
||||||
|
const img = makeImage(2, 1, [
|
||||||
|
[255, 0, 0, 255],
|
||||||
|
[0, 0, 255, 255]
|
||||||
|
]);
|
||||||
|
expect(sampleBilinear(img, 0, 0)).toEqual([255, 0, 0, 255]);
|
||||||
|
expect(sampleBilinear(img, 1, 0)).toEqual([0, 0, 255, 255]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('дробная координата интерполирует', () => {
|
||||||
|
const img = makeImage(2, 1, [
|
||||||
|
[0, 0, 0, 255],
|
||||||
|
[200, 200, 200, 255]
|
||||||
|
]);
|
||||||
|
const [r] = sampleBilinear(img, 0.5, 0);
|
||||||
|
expect(r).toBeCloseTo(100, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('координаты за краем клампятся', () => {
|
||||||
|
const img = makeImage(1, 1, [[7, 7, 7, 255]]);
|
||||||
|
expect(sampleBilinear(img, -10, -10)).toEqual([7, 7, 7, 255]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('rotateFreeImage', () => {
|
||||||
|
it('поворот на 180° даёт размеры не меньше исходных и непустой результат', () => {
|
||||||
|
const img = makeImage(4, 3, [
|
||||||
|
[255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255], [255, 255, 0, 255],
|
||||||
|
[128, 128, 128, 255], [64, 64, 64, 255], [32, 32, 32, 255], [200, 200, 200, 255],
|
||||||
|
[10, 20, 30, 255], [40, 50, 60, 255], [70, 80, 90, 255], [100, 100, 100, 255]
|
||||||
|
]);
|
||||||
|
const out = rotateFreeImage(img, 180);
|
||||||
|
expect(out.width).toBeGreaterThanOrEqual(img.width);
|
||||||
|
expect(out.height).toBeGreaterThanOrEqual(img.height);
|
||||||
|
let opaque = 0;
|
||||||
|
const total = out.width * out.height;
|
||||||
|
for (let i = 3; i < out.data.length; i += 4) {
|
||||||
|
if (out.data[i] > 0) opaque++;
|
||||||
|
}
|
||||||
|
expect(opaque / total).toBeGreaterThan(0.8);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('поворот квадрата на 360° близок к оригиналу', () => {
|
||||||
|
const img = makeImage(
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
new Array(25).fill([200, 100, 50, 255])
|
||||||
|
);
|
||||||
|
const out = rotateFreeImage(img, 360);
|
||||||
|
expect(out.width).toBeGreaterThanOrEqual(5);
|
||||||
|
expect(out.height).toBeGreaterThanOrEqual(5);
|
||||||
|
for (let y = 0; y < 5; y++) {
|
||||||
|
for (let x = 0; x < 5; x++) {
|
||||||
|
const di = (y * out.width + x) * 4;
|
||||||
|
for (let ch = 0; ch < 4; ch++) {
|
||||||
|
expect(Math.abs(out.data[di + ch] - img.data[(y * 5 + x) * 4 + ch])).toBeLessThanOrEqual(4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('skewImage', () => {
|
||||||
|
it('наклон X=45° сдвигает верхний правый угол', () => {
|
||||||
|
const img = makeImage(2, 2, [
|
||||||
|
[255, 0, 0, 255], [0, 255, 0, 255],
|
||||||
|
[0, 0, 255, 255], [128, 128, 128, 255]
|
||||||
|
]);
|
||||||
|
const out = skewImage(img, 45, 0);
|
||||||
|
expect(out.width).toBeGreaterThan(2);
|
||||||
|
expect(out.data[3]).toBe(255);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('углы 0° — тождественное преобразование', () => {
|
||||||
|
const img = makeImage(2, 2, [
|
||||||
|
[255, 0, 0, 255], [0, 255, 0, 255],
|
||||||
|
[0, 0, 255, 255], [128, 128, 128, 255]
|
||||||
|
]);
|
||||||
|
const out = skewImage(img, 0, 0);
|
||||||
|
expect(out.width).toBe(2);
|
||||||
|
expect([...out.data]).toEqual([...img.data]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
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 Error('Вырожденная матрица трансформации');
|
||||||
|
}
|
||||||
|
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)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function mulAffine(m1: AffineMatrix, m2: AffineMatrix): AffineMatrix {
|
||||||
|
return [
|
||||||
|
m1[0] * m2[0] + m1[2] * m2[1],
|
||||||
|
m1[1] * m2[0] + m1[3] * m2[1],
|
||||||
|
m1[0] * m2[2] + m1[2] * m2[3],
|
||||||
|
m1[1] * m2[2] + m1[3] * m2[3],
|
||||||
|
m1[0] * m2[4] + m1[2] * m2[5] + m1[4],
|
||||||
|
m1[1] * m2[4] + m1[3] * m2[5] + m1[5]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformImage(
|
||||||
|
img: PixelImage,
|
||||||
|
dstToSrc: AffineMatrix,
|
||||||
|
outWidth: number,
|
||||||
|
outHeight: number
|
||||||
|
): PixelImage {
|
||||||
|
const [a, b, c, d, e, f] = invertAffine(dstToSrc);
|
||||||
|
const out = createPixelImage(outWidth, outHeight);
|
||||||
|
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 < -1 || sy < -1 || sx > img.width || sy > img.height) continue;
|
||||||
|
const [r, g, bl, al] = sampleBilinear(img, sx, sy);
|
||||||
|
out.data[di] = r;
|
||||||
|
out.data[di + 1] = g;
|
||||||
|
out.data[di + 2] = bl;
|
||||||
|
out.data[di + 3] = al;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformCorners(
|
||||||
|
img: PixelImage,
|
||||||
|
m: AffineMatrix
|
||||||
|
): { minX: number; minY: number; outW: number; outH: number } {
|
||||||
|
const pts = [
|
||||||
|
[0, 0],
|
||||||
|
[img.width, 0],
|
||||||
|
[0, img.height],
|
||||||
|
[img.width, img.height]
|
||||||
|
].map(([x, y]) => [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]]);
|
||||||
|
const xs = pts.map((p) => p[0]);
|
||||||
|
const ys = pts.map((p) => p[1]);
|
||||||
|
const minX = Math.min(...xs);
|
||||||
|
const minY = Math.min(...ys);
|
||||||
|
return {
|
||||||
|
minX,
|
||||||
|
minY,
|
||||||
|
outW: Math.ceil(Math.max(...xs) - minX),
|
||||||
|
outH: Math.ceil(Math.max(...ys) - minY)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Error('Углы наклона не могут быть 90° или -90°');
|
||||||
|
}
|
||||||
|
const forward: AffineMatrix = [1, ky, kx, 1, 0, 0];
|
||||||
|
const bounds = transformCorners(img, forward);
|
||||||
|
const toOrigin: AffineMatrix = [1, 0, 0, 1, -bounds.minX, -bounds.minY];
|
||||||
|
return transformImage(img, mulAffine(invertAffine(forward), invertAffine(toOrigin)), bounds.outW, bounds.outH);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rotateFreeImage(img: PixelImage, degrees: number): PixelImage {
|
||||||
|
const rad = (degrees * Math.PI) / 180;
|
||||||
|
const cos = Math.cos(rad);
|
||||||
|
const sin = Math.sin(rad);
|
||||||
|
const forward: AffineMatrix = [cos, sin, -sin, cos, 0, 0];
|
||||||
|
const bounds = transformCorners(img, forward);
|
||||||
|
const toOrigin: AffineMatrix = [1, 0, 0, 1, -bounds.minX, -bounds.minY];
|
||||||
|
return transformImage(
|
||||||
|
img,
|
||||||
|
mulAffine(invertAffine(forward), invertAffine(toOrigin)),
|
||||||
|
bounds.outW,
|
||||||
|
bounds.outH
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import {
|
import {
|
||||||
|
autoContrast,
|
||||||
brightnessContrast,
|
brightnessContrast,
|
||||||
changeHue,
|
changeHue,
|
||||||
extractChannel,
|
extractChannel,
|
||||||
|
gammaCorrection,
|
||||||
grayscale,
|
grayscale,
|
||||||
invert,
|
invert,
|
||||||
posterize,
|
posterize,
|
||||||
@@ -10,6 +12,8 @@ import {
|
|||||||
sepia,
|
sepia,
|
||||||
setOpacity,
|
setOpacity,
|
||||||
swapChannels,
|
swapChannels,
|
||||||
|
temperature,
|
||||||
|
tint,
|
||||||
thresholdBlackWhite,
|
thresholdBlackWhite,
|
||||||
twoColors
|
twoColors
|
||||||
} from './color';
|
} from './color';
|
||||||
@@ -183,3 +187,48 @@ describe('brightnessContrast', () => {
|
|||||||
expect(out.data[3]).toBe(64);
|
expect(out.data[3]).toBe(64);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('gammaCorrection', () => {
|
||||||
|
it('гамма 1 — идентичность', () => {
|
||||||
|
const img = makeImage(1, 1, [[64, 128, 192, 255]]);
|
||||||
|
expect([...gammaCorrection(img, 1).data]).toEqual([64, 128, 192, 255]);
|
||||||
|
});
|
||||||
|
it('гамма 2 удваивает яркость (64 → 128)', () => {
|
||||||
|
const out = gammaCorrection(makeImage(1, 1, [[64, 64, 64, 255]]), 2);
|
||||||
|
expect(out.data[0]).toBe(128);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('autoContrast', () => {
|
||||||
|
it('растягивает диапазон [10..200] до [0..255]', () => {
|
||||||
|
const out = autoContrast(makeImage(2, 1, [
|
||||||
|
[10, 10, 10, 255],
|
||||||
|
[200, 200, 200, 255]
|
||||||
|
]));
|
||||||
|
expect(out.data[0]).toBe(0);
|
||||||
|
expect(out.data[4]).toBe(255);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('temperature', () => {
|
||||||
|
it('положительная — теплее (красный ↑, синий ↓)', () => {
|
||||||
|
const out = temperature(makeImage(1, 1, [[128, 128, 128, 255]]), 50);
|
||||||
|
expect(out.data[0]).toBeGreaterThan(128);
|
||||||
|
expect(out.data[2]).toBeLessThan(128);
|
||||||
|
});
|
||||||
|
it('нулевая температура не меняет', () => {
|
||||||
|
const img = makeImage(1, 1, [[128, 128, 128, 255]]);
|
||||||
|
expect([...temperature(img, 0).data]).toEqual([...img.data]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tint', () => {
|
||||||
|
it('сила 100 на белом даёт чистый цвет', () => {
|
||||||
|
const out = tint(makeImage(1, 1, [[255, 255, 255, 255]]), '#ff0000', 100);
|
||||||
|
expect([...out.data]).toEqual([255, 0, 0, 255]);
|
||||||
|
});
|
||||||
|
it('сила 0 — идентичность', () => {
|
||||||
|
const img = makeImage(1, 1, [[100, 150, 200, 255]]);
|
||||||
|
expect([...tint(img, '#ff0000', 0).data]).toEqual([...img.data]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -223,3 +223,84 @@ export function rgbToHex(r: number, g: number, b: number): string {
|
|||||||
function clamp(value: number, min: number, max: number): number {
|
function clamp(value: number, min: number, max: number): number {
|
||||||
return Math.min(max, Math.max(min, value));
|
return Math.min(max, Math.max(min, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function gammaCorrection(img: PixelImage, value: number): PixelImage {
|
||||||
|
const g = clamp(value, 0.1, 5);
|
||||||
|
const lut = new Uint8ClampedArray(256);
|
||||||
|
for (let v = 0; v < 256; v++) {
|
||||||
|
lut[v] = 255 * Math.pow(v / 255, 1 / g);
|
||||||
|
}
|
||||||
|
const out = createPixelImage(img.width, img.height);
|
||||||
|
for (let i = 0; i < out.data.length; i += 4) {
|
||||||
|
out.data[i] = lut[img.data[i]];
|
||||||
|
out.data[i + 1] = lut[img.data[i + 1]];
|
||||||
|
out.data[i + 2] = lut[img.data[i + 2]];
|
||||||
|
out.data[i + 3] = img.data[i + 3];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function autoContrast(img: PixelImage): PixelImage {
|
||||||
|
const lo = [255, 255, 255];
|
||||||
|
const hi = [0, 0, 0];
|
||||||
|
for (let i = 0; i < img.data.length; i += 4) {
|
||||||
|
for (let ch = 0; ch < 3; ch++) {
|
||||||
|
if (img.data[i + ch] < lo[ch]) lo[ch] = img.data[i + ch];
|
||||||
|
if (img.data[i + ch] > hi[ch]) hi[ch] = img.data[i + ch];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const luts: Uint8ClampedArray[] = [];
|
||||||
|
for (let ch = 0; ch < 3; ch++) {
|
||||||
|
const lut = new Uint8ClampedArray(256);
|
||||||
|
const range = hi[ch] - lo[ch];
|
||||||
|
for (let v = 0; v < 256; v++) {
|
||||||
|
lut[v] = range > 0 ? ((v - lo[ch]) * 255) / range : v;
|
||||||
|
}
|
||||||
|
luts.push(lut);
|
||||||
|
}
|
||||||
|
const out = createPixelImage(img.width, img.height);
|
||||||
|
for (let i = 0; i < out.data.length; i += 4) {
|
||||||
|
out.data[i] = luts[0][img.data[i]];
|
||||||
|
out.data[i + 1] = luts[1][img.data[i + 1]];
|
||||||
|
out.data[i + 2] = luts[2][img.data[i + 2]];
|
||||||
|
out.data[i + 3] = img.data[i + 3];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function temperature(img: PixelImage, percent: number): PixelImage {
|
||||||
|
const k = clamp(percent, -100, 100) / 100;
|
||||||
|
const rFactor = 1 + 0.25 * k;
|
||||||
|
const bFactor = 1 - 0.25 * k;
|
||||||
|
const out = createPixelImage(img.width, img.height);
|
||||||
|
for (let i = 0; i < out.data.length; i += 4) {
|
||||||
|
out.data[i] = img.data[i] * rFactor;
|
||||||
|
out.data[i + 1] = img.data[i + 1];
|
||||||
|
out.data[i + 2] = img.data[i + 2] * bFactor;
|
||||||
|
out.data[i + 3] = img.data[i + 3];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tint(
|
||||||
|
img: PixelImage,
|
||||||
|
colorHex: string,
|
||||||
|
strengthPercent: number
|
||||||
|
): PixelImage {
|
||||||
|
const s = clamp(strengthPercent, 0, 100) / 100;
|
||||||
|
const match = /^#([0-9a-f]{6})$/i.exec(colorHex.trim());
|
||||||
|
if (!match) throw new Error(`Некорректный HEX-цвет: "${colorHex}"`);
|
||||||
|
const d = match[1];
|
||||||
|
const tr = parseInt(d.slice(0, 2), 16) / 255;
|
||||||
|
const tg = parseInt(d.slice(2, 4), 16) / 255;
|
||||||
|
const tb = parseInt(d.slice(4, 6), 16) / 255;
|
||||||
|
const factors = [1 + (tr - 1) * s, 1 + (tg - 1) * s, 1 + (tb - 1) * s];
|
||||||
|
const out = createPixelImage(img.width, img.height);
|
||||||
|
for (let i = 0; i < out.data.length; i += 4) {
|
||||||
|
for (let ch = 0; ch < 3; ch++) {
|
||||||
|
out.data[i + ch] = img.data[i + ch] * factors[ch];
|
||||||
|
}
|
||||||
|
out.data[i + 3] = img.data[i + 3];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { vignette } from './effects';
|
||||||
|
import { makeImage } from './test-helpers';
|
||||||
|
|
||||||
|
describe('vignette', () => {
|
||||||
|
it('сила 0 — идентичное преобразование', () => {
|
||||||
|
const img = makeImage(3, 3, new Array(9).fill([200, 100, 50, 255]));
|
||||||
|
expect([...vignette(img, 0).data]).toEqual([...img.data]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('углы темнее центра', () => {
|
||||||
|
const img = makeImage(7, 7, new Array(49).fill([200, 200, 200, 255]));
|
||||||
|
const out = vignette(img, 80);
|
||||||
|
const centerR = out.data[(3 * 7 + 3) * 4];
|
||||||
|
const cornerR = out.data[0];
|
||||||
|
expect(cornerR).toBeLessThan(centerR);
|
||||||
|
expect(centerR).toBeGreaterThan(150);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { clonePixelImage, createPixelImage, type PixelImage } from './types';
|
||||||
|
|
||||||
|
export function vignette(img: PixelImage, strengthPercent: number): PixelImage {
|
||||||
|
const strength = Math.min(Math.max(strengthPercent, 0), 100) / 100;
|
||||||
|
if (strength === 0) return clonePixelImage(img);
|
||||||
|
const cx = (img.width - 1) / 2;
|
||||||
|
const cy = (img.height - 1) / 2;
|
||||||
|
const maxDist = Math.sqrt(cx * cx + cy * cy);
|
||||||
|
const out = createPixelImage(img.width, img.height);
|
||||||
|
for (let y = 0; y < img.height; y++) {
|
||||||
|
for (let x = 0; x < img.width; x++) {
|
||||||
|
const dx = x - cx;
|
||||||
|
const dy = y - cy;
|
||||||
|
const t = Math.sqrt(dx * dx + dy * dy) / maxDist;
|
||||||
|
const factor = 1 - strength * t * t;
|
||||||
|
const di = (y * img.width + x) * 4;
|
||||||
|
out.data[di] = img.data[di] * factor;
|
||||||
|
out.data[di + 1] = img.data[di + 1] * factor;
|
||||||
|
out.data[di + 2] = img.data[di + 2] * factor;
|
||||||
|
out.data[di + 3] = img.data[di + 3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
@@ -181,6 +181,34 @@ function copyPixel(src: PixelImage, sx: number, sy: number, dst: PixelImage, dx:
|
|||||||
dst.data[di + 3] = src.data[si + 3];
|
dst.data[di + 3] = src.data[si + 3];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sampleBilinear(
|
||||||
|
img: PixelImage,
|
||||||
|
fx: number,
|
||||||
|
fy: number
|
||||||
|
): [number, number, number, number] {
|
||||||
|
const maxX = img.width - 1;
|
||||||
|
const maxY = img.height - 1;
|
||||||
|
const cx = Math.min(Math.max(fx, 0), maxX);
|
||||||
|
const cy = Math.min(Math.max(fy, 0), maxY);
|
||||||
|
const x0 = Math.floor(cx);
|
||||||
|
const y0 = Math.floor(cy);
|
||||||
|
const tx = cx - x0;
|
||||||
|
const ty = cy - y0;
|
||||||
|
const x1 = Math.min(x0 + 1, maxX);
|
||||||
|
const y1 = Math.min(y0 + 1, maxY);
|
||||||
|
const i00 = (y0 * img.width + x0) * 4;
|
||||||
|
const i10 = (y0 * img.width + x1) * 4;
|
||||||
|
const i01 = (y1 * img.width + x0) * 4;
|
||||||
|
const i11 = (y1 * img.width + x1) * 4;
|
||||||
|
const result: [number, number, number, number] = [0, 0, 0, 0];
|
||||||
|
for (let ch = 0; ch < 4; ch++) {
|
||||||
|
const top = (1 - tx) * img.data[i00 + ch] + tx * img.data[i10 + ch];
|
||||||
|
const bottom = (1 - tx) * img.data[i01 + ch] + tx * img.data[i11 + ch];
|
||||||
|
result[ch] = (1 - ty) * top + ty * bottom;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function clampInt(value: number, min: number, max: number): number {
|
function clampInt(value: number, min: number, max: number): number {
|
||||||
return Math.min(max, Math.max(min, Math.trunc(value)));
|
return Math.min(max, Math.max(min, Math.trunc(value)));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user