vtf-js
Version:
A javascript IO library for the Valve Texture Format.
171 lines (170 loc) • 7.85 kB
JavaScript
function sinc(x) {
if (x === 0)
return 1.0;
const a = Math.PI * x;
return Math.sin(a) / a;
}
// Ported from zimg
// https://github.com/sekrit-twc/zimg/blob/6d52c3a1d63109f209af9e6ffa879f23d0ec7f02/src/zimg/resize/filter.cpp#L147
function make_bicubic(b, c) {
const p0 = (6 - b * 2) / 6;
const p2 = (-18 + b * 12 + c * 6) / 6;
const p3 = (12 - b * 9 - c * 6) / 6;
const q0 = (b * 8 + c * 24) / 6;
const q1 = (-b * 12 - c * 48) / 6;
const q2 = (b * 6 + c * 30) / 6;
const q3 = (-b - c * 6) / 6;
// console.log(`if (x < 1) return ${p0} + ${p2} * (x*x) + ${p3} * (x*x*x);
// if (x < 2) return ${q0} + ${q1} * x + ${q2} * (x*x) + ${q3} * (x*x*x);
// return 0;`);
return (x) => {
if (x < 1)
return p0 + p2 * (x * x) + p3 * (x * x * x);
if (x < 2)
return q0 + q1 * x + q2 * (x * x) + q3 * (x * x * x);
return 0;
};
}
// function stbi_cubic(x: number) {
// if (x < 1) return (4 * x*x*(3*x - 6)) / 6;
// if (x < 2) return (8 * x*(-12 + x*(6 - x))) / 6;
// return 0;
// }
// Some of the below was adapted from the resize-rs project.
// https://github.com/PistonDevelopers/resize/blob/master/src/lib.rs
/** @see {@link Filter} */
export const VFilters = {
/** Point filtering - Always picks the nearest pixel when resampling. */
Point: { radius: 0, kernel: () => 1.0 },
/** Triangle/bilinear filtering - Blends the four pixels surrounding a given point. */
Triangle: { radius: 1, kernel: x => Math.max(0, 1 - x) },
/** Box filtering - Evenly blends in the four closest pixels. */
Box: { radius: 1, kernel: x => x < 0.5 ? 1.0 : 0.0 },
Mitchell: { radius: 2, kernel: make_bicubic(1 / 3, 1 / 3) },
CatRom: { radius: 2, kernel: make_bicubic(0.0, 0.5) },
/** Lanczos-3 filtering - A sinc filter that acts identically to Valve's NICE filter. */
Lanczos3: { radius: 3, kernel: x => x < 3.0 ? sinc(x) * sinc(x / 3.0) : 0.0 },
};
export class VImageScaler {
src_width;
src_height;
dest_width;
dest_height;
filter;
coeffs_w;
coeffs_h;
constructor(src_width, src_height, dest_width, dest_height, filter) {
this.src_width = src_width;
this.src_height = src_height;
this.dest_width = dest_width;
this.dest_height = dest_height;
this.filter = filter;
const coeff_cache = {};
this.coeffs_w = this.calc_coeffs(src_width, dest_width, this.filter, coeff_cache);
if (src_width === src_height && dest_width === dest_height) {
this.coeffs_h = this.coeffs_w;
}
else {
this.coeffs_h = this.calc_coeffs(src_height, dest_height, this.filter, coeff_cache);
}
}
calc_coeffs(size1, size2, filter, cache) {
const inv_ratio = size1 / size2;
const filter_scale = Math.max(1, inv_ratio);
const filter_radius = filter_scale * filter.radius;
const filter_kernel = filter.kernel;
const coeffs = new Array(size2);
for (let x2 = 0; x2 < size2; x2++) {
// The (float) center of the filter in the src image
// The rest of this code assumes the pixels' "center" is the left side
const center_f = (x2 + 0.5) * inv_ratio - 0.5;
// The pixel indices where the window starts/stops in the src image
const start = Math.max(0, Math.floor(center_f - filter_radius));
const end = Math.max(start + 1, Math.min(size1, Math.ceil(center_f + filter_radius)));
const length = end - start;
if (length <= 0)
throw `Got length of ${length} with filter of radius ${filter.radius} at position ${center_f}`;
const offset_from_center = center_f - start;
const cache_key = filter_scale + ',' + length.toString(36) + ',' + offset_from_center;
// Reuse the same coeffs whenever possible for perf!!
if (cache_key in cache) {
coeffs[x2] = { start, coeffs: cache[cache_key] };
continue;
}
const pixel_coeffs = new Float32Array(length);
cache[cache_key] = pixel_coeffs;
coeffs[x2] = { start, coeffs: pixel_coeffs };
let pixel_coeffs_sum = 0;
for (let i = 0; i < length; i++) {
const distance = Math.abs(i - offset_from_center);
const influence = filter_kernel(distance / filter_scale);
pixel_coeffs[i] = influence;
pixel_coeffs_sum += influence;
}
for (let i = 0; i < length; i++)
pixel_coeffs[i] /= pixel_coeffs_sum;
}
return coeffs;
}
resize(src, dst) {
if (src.width !== this.src_width || src.height !== this.src_height)
throw Error(`VImageScaler.resize input does not match expected dimensions! (expected ${this.src_width}x${this.src_height} but got ${src.width}x${src.height})`);
if (dst.width !== this.dest_width || dst.height !== this.dest_height)
throw Error(`VImageScaler.resize output does not match expected dimensions! (expected ${this.dest_width}x${this.dest_height} but got ${dst.width}x${dst.height})`);
if (dst.data.length !== this.dest_width * this.dest_height * 4)
throw Error(`VImageScaler.resize output data length should be ${this.dest_width * this.dest_height * 4}, got ${dst.data.length} instead!`);
// Used for accumulating since Uint8Arrays always round down (which means a totally black image)
let tmp_r = 0.0, tmp_g = 0.0, tmp_b = 0.0, tmp_a = 0.0;
const tmp0 = src.data;
const tmp1 = new (src.getDataConstructor())(this.dest_width * this.src_height * 4);
// Resize from (w1, h1) to (w2, h1)
for (let y = 0; y < this.src_height; y++) {
for (let x = 0; x < this.dest_width; x++) {
const i = (y * this.dest_width + x) * 4;
tmp_r = 0;
tmp_g = 0;
tmp_b = 0;
tmp_a = 0;
const { coeffs, start: coeffs_start } = this.coeffs_w[x];
for (let c = 0; c < coeffs.length; c++) {
const coeff_i = (y * this.src_width + (coeffs_start + c)) * 4;
const coeff = coeffs[c];
tmp_r += tmp0[coeff_i] * coeff;
tmp_g += tmp0[coeff_i + 1] * coeff;
tmp_b += tmp0[coeff_i + 2] * coeff;
tmp_a += tmp0[coeff_i + 3] * coeff;
}
tmp1[i] = tmp_r;
tmp1[i + 1] = tmp_g;
tmp1[i + 2] = tmp_b;
tmp1[i + 3] = tmp_a;
}
}
// const tmp2 = new pixel_array(this.dest_width * this.src_height);
const tmp2 = dst.data;
// Resize from (w2, h1) to (w2, h2)
for (let y = 0; y < this.dest_height; y++) {
for (let x = 0; x < this.dest_width; x++) {
const i = (y * this.dest_width + x) * 4;
tmp_r = 0;
tmp_g = 0;
tmp_b = 0;
tmp_a = 0;
const { coeffs, start: coeffs_start } = this.coeffs_h[y];
for (let c = 0; c < coeffs.length; c++) {
const coeff_i = ((coeffs_start + c) * this.dest_width + x) * 4;
const coeff = coeffs[c];
tmp_r += tmp1[coeff_i] * coeff;
tmp_g += tmp1[coeff_i + 1] * coeff;
tmp_b += tmp1[coeff_i + 2] * coeff;
tmp_a += tmp1[coeff_i + 3] * coeff;
}
tmp2[i] = tmp_r;
tmp2[i + 1] = tmp_g;
tmp2[i + 2] = tmp_b;
tmp2[i + 3] = tmp_a;
}
}
return dst;
}
}