@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
434 lines (353 loc) • 11 kB
JavaScript
export class PNG {
/**
*
* @type {number}
*/
width = 0;
/**
*
* @type {number}
*/
height = 0;
/**
* Number of bits per channel?
* @type {number}
*/
bitDepth = 0;
colorType = 0;
compressionMethod = 0;
filterMethod = 0;
interlaceMethod = 0;
/**
* Number of channels in the image
* @example RGB = 3, RGBA = 4, etc.
* @type {number}
*/
colors = 0;
alpha = false;
/**
*
* @type {Uint8Array|null}
*/
palette = null;
/**
*
* @type {Uint8Array|null}
*/
pixels = null;
/**
* Transparency palette
* @type {Uint8Array|null}
*/
transparency_lookup = null;
/**
* Text metadata coming from tEXt chunks
* @type {Object<string>}
*/
text = {};
getWidth() {
return this.width;
}
setWidth(width) {
this.width = width;
}
getHeight() {
return this.height;
}
setHeight(height) {
this.height = height;
}
getBitDepth() {
return this.bitDepth;
}
setBitDepth(bitDepth) {
if ([1, 2, 4, 8, 16].indexOf(bitDepth) === -1) {
throw new Error("invalid bith depth " + bitDepth);
}
this.bitDepth = bitDepth;
}
getColorType() {
return this.colorType;
}
setColorType(colorType) {
// Color Allowed Interpretation
// Type Bit Depths
//
// 0 1,2,4,8,16 Each pixel is a grayscale sample.
//
// 2 8,16 Each pixel is an R,G,B triple.
//
// 3 1,2,4,8 Each pixel is a palette index;
// a PLTE chunk must appear.
//
// 4 8,16 Each pixel is a grayscale sample,
// followed by an alpha sample.
//
// 6 8,16 Each pixel is an R,G,B triple,
// followed by an alpha sample.
let colors = 0, alpha = false;
switch (colorType) {
case 0:
colors = 1;
break;
case 2:
colors = 3;
break;
case 3:
colors = 1;
break;
case 4:
colors = 2;
alpha = true;
break;
case 6:
colors = 4;
alpha = true;
break;
default:
throw new Error("invalid color type");
}
this.colors = colors;
this.alpha = alpha;
this.colorType = colorType;
}
/**
*
* @param {number} compressionMethod
*/
setCompressionMethod(compressionMethod) {
if (compressionMethod !== 0) {
throw new Error("invalid compression method " + compressionMethod);
}
this.compressionMethod = compressionMethod;
}
/**
*
* @param {number} filterMethod
*/
setFilterMethod(filterMethod) {
if (filterMethod !== 0) {
throw new Error("invalid filter method " + filterMethod);
}
this.filterMethod = filterMethod;
}
getInterlaceMethod() {
return this.interlaceMethod;
}
setInterlaceMethod(interlaceMethod) {
if (interlaceMethod !== 0 && interlaceMethod !== 1) {
throw new Error("invalid interlace method " + interlaceMethod);
}
this.interlaceMethod = interlaceMethod;
}
/**
*
* @param {Uint8Array} palette
*/
setPalette(palette) {
if (palette.length % 3 !== 0) {
throw new Error("incorrect PLTE chunk length");
}
if (palette.length > (Math.pow(2, this.bitDepth) * 3)) {
throw new Error("palette has more colors than 2^bitdepth");
}
this.palette = palette;
}
/**
* get the pixel color on a certain location in a normalized way
* result is an array: [red, green, blue, alpha]
*/
getPixel(result, result_offset, x, y) {
const pixels = this.pixels;
if (!pixels) {
throw new Error("pixel data is empty");
}
if (x >= this.width || y >= this.height) {
throw new Error("x,y position out of bound");
}
const i = this.colors * this.bitDepth / 8 * (y * this.width + x);
let r, g, b, a;
switch (this.colorType) {
case 0:
r = pixels[i];
g = r;
b = r;
a = 255;
break;
case 2:
r = pixels[i];
g = pixels[i + 1];
b = pixels[i + 2];
a = 255;
break;
case 3:
a = 255;
if (this.transparency_lookup != null) {
a = this.transparency_lookup[pixels[i]];
}
const offset = pixels[i] * 3;
const palette = this.palette;
r = palette[offset];
g = palette[offset + 1];
b = palette[offset + 2];
break;
case 4:
r = pixels[i];
g = r;
b = r;
a = pixels[i + 1];
break;
case 6:
r = pixels[i];
g = pixels[i + 1];
b = pixels[i + 2];
a = pixels[i + 3];
break;
default:
throw new Error('Unsupported color type');
}
result[result_offset + 0] = r;
result[result_offset + 1] = g;
result[result_offset + 2] = b;
result[result_offset + 3] = a;
}
/**
* Assumes pixels are stored as RGB without A component, will set A to 255
* @param {Uint8Array} destination
*/
getRGBA8Array_fromRGB(destination) {
const height = this.height;
const width = this.width;
const pixel_count = width * height;
const source = this.pixels;
for (let i = 0; i < pixel_count; i++) {
const i3 = i * 3;
const i4 = i3 + i;
destination[i4] = source[i3];
destination[i4 + 1] = source[i3 + 1];
destination[i4 + 2] = source[i3 + 2];
destination[i4 + 3] = 255;
}
}
/**
*
* @param {Uint8Array} destination
*/
getRGBA8Array_generic(destination) {
const height = this.height;
const width = this.width;
for (let y = 0; y < height; y++) {
const row_index = y * width;
for (let x = 0; x < width; x++) {
const address = (row_index + x) * 4;
this.getPixel(destination, address, x, y);
}
}
}
/**
* get the pixels of the image as a RGBA array of the form [r1, g1, b1, a1, r2, b2, g2, a2, ...]
* Matches the api of canvas.getImageData
*/
getRGBA8Array() {
if (this.colorType === 6) {
// RGBA color type, return pixels directly
return this.pixels;
}
const height = this.height;
const width = this.width;
const data = new Uint8Array(width * height * 4);
if (this.colorType === 2) {
// RGB color type
this.getRGBA8Array_fromRGB(data);
} else {
// original, slow generic method
this.getRGBA8Array_generic(data);
}
return data;
}
getUint8Data_case3() {
const w = this.width;
const h = this.height;
const area = w * h;
let itemSize;
const transparency_lookup = this.transparency_lookup;
if (transparency_lookup !== null) {
// has transparency
itemSize = 4;
} else {
itemSize = 3;
}
const result_data = new Uint8Array(area * itemSize);
const pixels = this.pixels;
const palette = this.palette;
const d = this.colors * Math.ceil(this.bitDepth / 8);
for (let i = 0; i < area; i++) {
const destination_address = i * itemSize;
const lookup_index = pixels[i * d];
const lookup_value = lookup_index * 3;
result_data[destination_address] = palette[lookup_value];
result_data[destination_address + 1] = palette[lookup_value + 1];
result_data[destination_address + 2] = palette[lookup_value + 2];
}
//transparency
if (transparency_lookup !== null) {
const transparency_lookup_size = transparency_lookup.length;
for (let i = 0; i < area; i++) {
const pixel_index = pixels[i * d];
const result_address = i * 4 + 3;
if (pixel_index >= transparency_lookup_size) {
/*
when sampling outside of lookup, value defaults to 255
@see "tRNS" chunk in PNG 1.2 spec
*/
result_data[result_address] = 255;
} else {
result_data[result_address] = transparency_lookup[pixel_index];
}
}
}
return {
data: result_data,
itemSize: itemSize
}
}
/**
* @returns {{itemSize:number, data:Uint8Array, bitDepth: number}}
*/
getUint8Data() {
let data;
let itemSize = 0; // note, can take this from this.colors
const color_type = this.colorType;
switch (color_type) {
case 0:
data = this.pixels;
itemSize = 1;
break;
case 2:
data = this.pixels;
itemSize = 3;
break;
case 3:
// palette
const c3 = this.getUint8Data_case3();
data = c3.data;
itemSize = c3.itemSize;
break;
case 4:
// grayscale with alpha
data = this.pixels;
itemSize = 2;
break;
case 6:
data = this.pixels;
itemSize = 4;
break;
default:
throw new Error('Unsupported color type');
}
return {
data,
itemSize
};
}
}