All files / src/shared/lib/tiles indexedImage.ts

83.08% Statements 113/136
58.33% Branches 14/24
92.3% Functions 12/13
81.08% Lines 90/111

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283                        71x 71x 71x 71x 71x 71x 71x                                                                     71x           165x 165x 165x 2660608x           2660608x   165x                 71x       55659x                       71x 101x                           71x 6606912x                     71x             55429x 55429x 55429x   55429x 55429x 443576x 3541600x 3534592x 3534592x   7008x   3541600x     55429x               71x 40716x 40716x 40716x 325728x 325728x 325728x 2605824x 2605824x 2605824x 2605824x   325728x 325728x 325728x 325728x     40716x               71x 24x 24x 24x 24x 24x 304x 2432x 2432x 2432x     24x               71x 24x 24x 24x 24x 24x 304x 2432x 2432x 2432x     24x     71x                                                                         71x       7008x 7008x 7008x 7008x 7008x 56064x 448512x 448512x 441504x 7008x   441504x 48180x         7008x 7008x 7008x           2605824x 651456x  
/**
 * A data wrapper for an image using indexed colors with one 8-bit value per pixel
 */
export type IndexedImage = {
  width: number;
  height: number;
  data: Uint8Array;
};
 
/**
 * A color value for use in IndexedImages
 */
enum Color {
  Transparent = 0,
  Light = 1,
  Mid = 2,
  Dark = 3,
  Divider = 254,
  Unknown = 255,
}
 
/**
 * A function for converting rgb pixel values to an IndexedImage Color
 */
export type ImageIndexFunction = (
  r: number,
  g: number,
  b: number,
  a: number,
) => number;
 
/**
 * A wrapper for a slice of an image giving the sliced data and coordinates where the slice was taken
 */
type SliceDef = {
  data: IndexedImage;
  coordinates: {
    x: number;
    y: number;
    width: number;
    height: number;
  };
};
 
/**
 * Convert an array of pixel data into an IndexedImage using a given index function
 *
 * @param width Image width
 * @param height Image height
 * @param pixels Raw RGB pixel data array
 * @param indexFn A function to convert RGB values to a color index
 * @returns A new IndexedImage representing the pixel data provided
 */
export const pixelDataToIndexedImage = (
  width: number,
  height: number,
  pixels: Buffer | Uint8ClampedArray,
  indexFn: ImageIndexFunction,
): IndexedImage => {
  const output = makeIndexedImage(width, height);
  let ii = 0;
  for (let i = 0; i < pixels.length; i += 4) {
    output.data[ii] = indexFn(
      pixels[i],
      pixels[i + 1],
      pixels[i + 2],
      pixels[i + 3],
    );
    ii++;
  }
  return output;
};
 
/**
 * Create a blank IndexedImage with a given width & height
 * @param width Image width
 * @param height Image Height
 * @returns IndexedImage with blank data
 */
export const makeIndexedImage = (
  width: number,
  height: number,
): IndexedImage => {
  return {
    width,
    height,
    data: new Uint8Array(width * height),
  };
};
 
/**
 * Create a deep clone of an IndexedImage
 * @param original An IndexedImage to clone
 * @returns A deep clone of the original IndexedImage
 */
export const cloneIndexedImage = (original: IndexedImage): IndexedImage => {
  return {
    width: original.width,
    height: original.height,
    data: new Uint8Array(original.data),
  };
};
 
/**
 * Convert an x/y coordinate to an index with an IndexedImage's data
 * @param x X coordinate
 * @param y Y coordinate
 * @param image IndexedImage to read
 * @returns Index value to using in image.data[]
 */
export const toIndex = (x: number, y: number, image: IndexedImage): number =>
  x + y * image.width;
 
/**
 * Create a new IndexedImage by slicing from a larger image.
 * @param inData IndexedImage to slice
 * @param startX Left coordinate
 * @param startY Top coordinate
 * @param width Width of slice
 * @param height Height of slice
 * @returns
 */
export const sliceIndexedImage = (
  inData: IndexedImage,
  startX: number,
  startY: number,
  width: number,
  height: number,
): IndexedImage => {
  const output = makeIndexedImage(width, height);
  const inWidth = inData.width;
  const inHeight = inData.height;
 
  let ii = 0;
  for (let y = startY; y < startY + height; y++) {
    for (let x = startX; x < startX + width; x++) {
      if (x < inWidth && y < inHeight && x >= 0 && y >= 0) {
        const i = toIndex(x, y, inData);
        output.data[ii] = inData.data[i];
      } else {
        output.data[ii] = Color.Transparent;
      }
      ii++;
    }
  }
  return output;
};
 
/**
 * Convert an 8px tile to GB 2bpp format
 * @param image Tile image to read (should be 8x8px)
 * @returns Array of GB format 2bpp image data
 */
export const indexedImageTo2bppTileData = (image: IndexedImage): Uint8Array => {
  const output = new Uint8Array(16);
  let i = 0;
  for (let y = 0; y < 8; y++) {
    let row1 = "";
    let row2 = "";
    for (let x = 0; x < 8; x++) {
      const index = toIndex(x, y, image);
      const binary = bin2(image.data[index]);
      row1 += binary[1];
      row2 += binary[0];
    }
    output[i] = binDec(row1);
    i++;
    output[i] = binDec(row2);
    i++;
  }
 
  return output;
};
 
/**
 * Flip IndexedImage horizontally
 * @param inData Image to flip
 * @returns Horizontally flipped IndexedImage
 */
export const flipIndexedImageX = (inData: IndexedImage): IndexedImage => {
  const output = makeIndexedImage(inData.width, inData.height);
  const width = inData.width;
  const height = inData.height;
  let ii = 0;
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      const i = toIndex(width - x - 1, y, inData);
      output.data[ii] = inData.data[i];
      ii++;
    }
  }
  return output;
};
 
/**
 * Flip IndexedImage vertically
 * @param inData Image to flip
 * @returns Vertically flipped IndexedImage
 */
export const flipIndexedImageY = (inData: IndexedImage): IndexedImage => {
  const output = makeIndexedImage(inData.width, inData.height);
  const width = inData.width;
  const height = inData.height;
  let ii = 0;
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      const i = toIndex(x, height - y - 1, inData);
      output.data[ii] = inData.data[i];
      ii++;
    }
  }
  return output;
};
 
export const trimIndexedImage = (
  inData: IndexedImage,
  trimValue: number,
): SliceDef => {
  const width = inData.width;
  const height = inData.height;
  let minX = width;
  let maxX = 0;
  let minY = height;
  let maxY = 0;
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      const i = toIndex(x, y, inData);
      if (inData.data[i] !== trimValue) {
        if (x < minX) {
          minX = x;
        }
        if (x > maxX) {
          maxX = x;
        }
        if (y < minY) {
          minY = y;
        }
        if (y > maxY) {
          maxY = y;
        }
      }
    }
  }
  const sliceW = Math.max(0, maxX - minX + 1);
  const sliceH = Math.max(0, maxY - minY + 1);
  return {
    data: sliceIndexedImage(inData, minX, minY, sliceW, sliceH),
    coordinates: { x: minX, y: minY, width: sliceW, height: sliceH },
  };
};
 
export const trimIndexedImageHorizontal = (
  inData: IndexedImage,
  trimValue: number,
): SliceDef => {
  const width = inData.width;
  const height = inData.height;
  let minX = width;
  let maxX = 0;
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      const i = toIndex(x, y, inData);
      if (inData.data[i] !== trimValue) {
        if (x < minX) {
          minX = x;
        }
        if (x > maxX) {
          maxX = x;
        }
      }
    }
  }
  const sliceW = Math.max(0, maxX - minX + 1);
  const sliceH = inData.height;
  return {
    data: sliceIndexedImage(inData, minX, 0, sliceW, sliceH),
    coordinates: { x: minX, y: 0, width: sliceW, height: sliceH },
  };
};
 
const bin2 = (value: Color) => value.toString(2).padStart(2, "0");
const binDec = (binary: string) => parseInt(binary, 2);