UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

434 lines (353 loc) • 11 kB
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 }; } }