utils.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. // If anything remains unclear check this video: https://youtu.be/txkHN6izK2Y?si=Mg0vllVL-3uzrHqh
  2. import JSZip from "jszip";
  3. import { saveAs } from "file-saver";
  4. import {
  5. DOWNLOAD_FILE_NAME,
  6. IMAGE_CANVAS_CLASSNAME,
  7. PHOTO_COUNT,
  8. PHOTO_HEIGHT,
  9. PHOTO_WIDTH,
  10. } from "./constants";
  11. export const getImageDataFromPhoto = (photoData, palette) => {
  12. // Photos consist of 8x8 pixel tiles
  13. // >> 3 gives the amount of 8x8 tiles for the size
  14. const wTiles = PHOTO_WIDTH >> 3; // 16 tiles
  15. const hTiles = PHOTO_HEIGHT >> 3; // 14 tiles
  16. // Gameboy image (pr camera photo) tile has 8 pixels in a row
  17. const pixelsPerRow = 8;
  18. // Create canvas imageData for storing RGBA values
  19. const imageData = new ImageData(PHOTO_WIDTH, PHOTO_HEIGHT);
  20. // Shade map will contain pixel indexes by shades for palette replacement in imageData in the future
  21. imageData.shadeMap = {
  22. shade0: [],
  23. shade1: [],
  24. shade2: [],
  25. shade3: [],
  26. };
  27. // Gameboy stores colors as nominal values, that need to be translated to a palette
  28. // This is a default 4 shades of grey palette, but we use the one from the props
  29. // const palette = [
  30. // [255, 255, 255], // White (shade 0)
  31. // [192, 192, 192], // Light Grey (shade 1)
  32. // [96, 96, 96], // Dark Grey (shade 2)
  33. // [0, 0, 0], // Black (shade 3)
  34. // ];
  35. // Go through each row ("y") and each tile by column ("x")
  36. for (let y = 0; y < hTiles * pixelsPerRow; y++) {
  37. // index of a tile by width
  38. for (let wTileIndex = 0; wTileIndex < wTiles; wTileIndex++) {
  39. const hTileIndex = y >> 3; // index of a tile by height
  40. const tileIndex = hTileIndex * wTiles + wTileIndex; // 1-dimensional index of a tile
  41. // 16 is a number of bytes per tile
  42. // We need to jump this amount when calculating indexes,
  43. // because for canvas image data pixels have to be stored in plain order,
  44. // but in Gameboy's photo data bytes (and therefore pixels) are ordered by tiles
  45. const byteIndexOffset = 16 * tileIndex;
  46. // Go through a row of 8 pixels (1 row of a tile)
  47. for (let i = 0; i < pixelsPerRow; i++) {
  48. // As photo data bytes are ordered by tiles,
  49. // we need to calculate byte indexes based on the current row ("y") and "byteIndexOffset"
  50. const byteIndex = byteIndexOffset + (y % pixelsPerRow) * 2; // 8 pixels per row and 2 bytes for pixel
  51. // Gameboy uses 2BPP (2 bits per pixel) storage system for pixel data
  52. // One byte consists of 2 bits and so contains 4 pixels data, so there're 2 bytes per row
  53. // The 2 bits for each pixel are split between 2 bytes
  54. // First byte contais each pixel's second bit, and the second byte contains each pixel's first bit
  55. // So in order to get the pixel data, we need to look at 2 bytes and determine the value (1 or 0) at a given bit ("i")
  56. // Combining those to values (here is comparing to 0 and incrementing "shade") we can apply palette
  57. // 0x80 gives us binary 10000000 and >> i moves it to the needed bit ("i")
  58. // & operator multiplies two binary numbers and determines if the photoData's bit at index "i" is 1 or 0
  59. let shade = 0;
  60. if ((photoData[byteIndex] & (0x80 >> i)) != 0) {
  61. shade += 1;
  62. }
  63. if ((photoData[byteIndex + 1] & (0x80 >> i)) != 0) {
  64. shade += 2;
  65. }
  66. // 1-dimensional index of a canvas image pixel, with a step of 4 for RGBA values
  67. const pixelX = wTileIndex * pixelsPerRow + i;
  68. const pixelIndex = (y * PHOTO_WIDTH + pixelX) * 4;
  69. imageData.data[pixelIndex + 0] = palette[shade][0];
  70. imageData.data[pixelIndex + 1] = palette[shade][1];
  71. imageData.data[pixelIndex + 2] = palette[shade][2];
  72. imageData.data[pixelIndex + 3] = 255;
  73. // Save indexes in shade map
  74. imageData.shadeMap[`shade${shade}`].push([
  75. pixelIndex + 0,
  76. pixelIndex + 1,
  77. pixelIndex + 2,
  78. ]);
  79. }
  80. }
  81. }
  82. return imageData;
  83. };
  84. export const replaceImageDataColor = (imageData, shadeIndex, newColor) => {
  85. const data = imageData.data;
  86. const [r, g, b] = newColor;
  87. imageData.shadeMap[`shade${shadeIndex}`].forEach((pixelIndex) => {
  88. data[pixelIndex[0]] = r;
  89. data[pixelIndex[1]] = g;
  90. data[pixelIndex[2]] = b;
  91. });
  92. };
  93. export const hexToRgb = (hex) => {
  94. // Remove the hash at the start if it's there
  95. hex = hex.replace(/^#/, "");
  96. let bigint = parseInt(hex, 16);
  97. let r = (bigint >> 16) & 255;
  98. let g = (bigint >> 8) & 255;
  99. let b = bigint & 255;
  100. return [r, g, b];
  101. };
  102. export const getScaledCanvas = (originalCanvas, scale) => {
  103. // Create an off-screen canvas for scaling
  104. const scaledCanvas = document.createElement("canvas");
  105. const ctx = scaledCanvas.getContext("2d");
  106. // Set the scaled canvas dimensions
  107. scaledCanvas.width = originalCanvas.width * scale;
  108. scaledCanvas.height = originalCanvas.height * scale;
  109. ctx.imageSmoothingEnabled = false;
  110. // Scale the original image onto the scaled canvas
  111. ctx.drawImage(
  112. originalCanvas,
  113. 0,
  114. 0,
  115. originalCanvas.width,
  116. originalCanvas.height,
  117. 0,
  118. 0,
  119. scaledCanvas.width,
  120. scaledCanvas.height
  121. );
  122. return scaledCanvas;
  123. };
  124. export const updatePalettePresetStorage = (updatedPresets) => {
  125. if (Object.keys(updatedPresets).length) {
  126. localStorage.setItem("palettePresets", JSON.stringify(updatedPresets));
  127. } else {
  128. localStorage.removeItem("palettePresets");
  129. }
  130. };
  131. export const getPalettePresetsFromStorage = () => {
  132. return JSON.parse(localStorage.getItem("palettePresets"));
  133. };
  134. export const downloadCurrent = (imageScale) => {
  135. const canvas = document.getElementsByClassName(IMAGE_CANVAS_CLASSNAME)?.[0];
  136. if (!canvas) return;
  137. const scaledCanvas = getScaledCanvas(canvas, imageScale);
  138. scaledCanvas.toBlob((blob) => saveAs(blob, `${DOWNLOAD_FILE_NAME}.png`), "image/png");
  139. };
  140. export const downloadAll = async (imageScale) => {
  141. const canvases = document.getElementsByClassName(IMAGE_CANVAS_CLASSNAME);
  142. if (!canvases) return;
  143. const zip = new JSZip();
  144. const zipPromises = [];
  145. for (let i = 1; i <= PHOTO_COUNT; i++) {
  146. const promise = new Promise((resolve) => {
  147. const scaledCanvas = getScaledCanvas(canvases[i], imageScale);
  148. scaledCanvas.toBlob((blob) => {
  149. zip.file(`${DOWNLOAD_FILE_NAME}-${i - 1}.png`, blob);
  150. resolve();
  151. }, "image/png");
  152. });
  153. zipPromises.push(promise);
  154. }
  155. await Promise.all(zipPromises);
  156. const zipBlob = await zip.generateAsync({ type: "blob" });
  157. saveAs(zipBlob, `${DOWNLOAD_FILE_NAME}s.zip`);
  158. };
  159. export const areArraysEqual = (arr1, arr2) => {
  160. return arr1.length === arr2.length && arr1.every((value, index) => value === arr2[index]);
  161. };