gen-test-map.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. // Generates the placeholder PNGs of the reference map "gateau-etages".
  2. // Usage: npm run gen:test-map
  3. import { mkdirSync, writeFileSync } from 'node:fs';
  4. import { resolve } from 'node:path';
  5. import { PNG } from 'pngjs';
  6. const W = 2048;
  7. const H = 1024;
  8. const outDir = resolve(import.meta.dirname, '../assets/maps/gateau-etages');
  9. const VOID = 0x000000;
  10. const SPONGE = 0xf2c46d;
  11. const CHOCOLATE = 0x5a3a22;
  12. const CREAM = 0xffffff;
  13. const JELLY = 0xd93a5b;
  14. const BISCUIT = 0xc8964b;
  15. const PLATE = 0x8899aa;
  16. const terrain = new Uint32Array(W * H).fill(VOID);
  17. const put = (x: number, y: number, c: number): void => {
  18. if (x >= 0 && y >= 0 && x < W && y < H) terrain[y * W + x] = c;
  19. };
  20. const rect = (x0: number, y0: number, x1: number, y1: number, c: number): void => {
  21. for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++) put(x, y, c);
  22. };
  23. // Plate: a wide dish with a raised rim.
  24. rect(180, 960, 1868, 990, PLATE);
  25. rect(150, 940, 200, 975, PLATE);
  26. rect(1848, 940, 1898, 975, PLATE);
  27. interface Tier {
  28. x0: number;
  29. x1: number;
  30. top: number;
  31. bottom: number;
  32. }
  33. const tiers: Tier[] = [
  34. { x0: 280, x1: 1768, top: 700, bottom: 960 },
  35. { x0: 520, x1: 1528, top: 470, bottom: 700 },
  36. { x0: 780, x1: 1268, top: 270, bottom: 470 },
  37. ];
  38. for (const t of tiers) {
  39. const h = t.bottom - t.top;
  40. rect(t.x0, t.top, t.x1, t.bottom, SPONGE);
  41. // Biscuit base, jelly layer and chocolate layer.
  42. rect(t.x0, t.bottom - Math.round(h * 0.12), t.x1, t.bottom, BISCUIT);
  43. rect(t.x0, t.top + Math.round(h * 0.55), t.x1, t.top + Math.round(h * 0.62), JELLY);
  44. rect(t.x0, t.top + Math.round(h * 0.3), t.x1, t.top + Math.round(h * 0.36), CHOCOLATE);
  45. // Chocolate pillar in the middle, a hard core to dig around.
  46. const mid = Math.round((t.x0 + t.x1) / 2);
  47. rect(mid - 20, t.top + 20, mid + 20, t.bottom - Math.round(h * 0.12), CHOCOLATE);
  48. // Whipped cream frosting with drips.
  49. for (let x = t.x0 - 6; x < t.x1 + 6; x++) {
  50. const wave = Math.round(6 + 4 * Math.sin(x / 23));
  51. for (let y = t.top - wave; y < t.top + 12; y++) put(x, y, CREAM);
  52. const drip = (x * 7919) % 97 < 6 ? 18 + ((x * 31) % 22) : 0;
  53. if (x >= t.x0 && x < t.x1) for (let y = t.top + 12; y < t.top + 12 + drip; y++) put(x, y, CREAM);
  54. }
  55. for (let y = t.top; y < t.top + 60; y++) {
  56. const len = Math.max(0, 6 - Math.floor((y - t.top) / 10));
  57. for (let i = 0; i < len; i++) {
  58. put(t.x0 - 6 + i, y, CREAM);
  59. put(t.x1 + 5 - i, y, CREAM);
  60. }
  61. }
  62. }
  63. function writePng(name: string, w: number, h: number, pixel: (x: number, y: number) => number): void {
  64. const png = new PNG({ width: w, height: h });
  65. for (let y = 0; y < h; y++) {
  66. for (let x = 0; x < w; x++) {
  67. const c = pixel(x, y);
  68. const i = (y * w + x) * 4;
  69. png.data[i] = (c >> 16) & 0xff;
  70. png.data[i + 1] = (c >> 8) & 0xff;
  71. png.data[i + 2] = c & 0xff;
  72. png.data[i + 3] = 0xff;
  73. }
  74. }
  75. writeFileSync(resolve(outDir, name), PNG.sync.write(png));
  76. }
  77. const lerp = (a: number, b: number, t: number): number => Math.round(a + (b - a) * t);
  78. const mix = (c1: number, c2: number, t: number): number =>
  79. (lerp(c1 >> 16, c2 >> 16, t) << 16) | (lerp((c1 >> 8) & 0xff, (c2 >> 8) & 0xff, t) << 8) | lerp(c1 & 0xff, c2 & 0xff, t);
  80. // ---------- Background: a cartoon kitchen, drawn with felt-pen outlines ----------
  81. const INK = 0x3b2414;
  82. const TABLE_Y = 900;
  83. function hash(x: number, y: number): number {
  84. let h = Math.imul(x, 374761393) ^ Math.imul(y, 668265263);
  85. h = Math.imul(h ^ (h >>> 13), 1274126177);
  86. return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
  87. }
  88. function paintBackground(): Uint32Array {
  89. const img = new Uint32Array(W * H);
  90. type Inside = (x: number, y: number) => boolean;
  91. /** Fills a shape (inside test over a bounding box) with a color function and a felt outline. */
  92. const shape = (x0: number, y0: number, x1: number, y1: number, inside: Inside, fill: (x: number, y: number) => number, outline = 4): void => {
  93. for (let y = Math.max(0, Math.floor(y0) - 1); y < Math.min(H, Math.ceil(y1) + 1); y++) {
  94. for (let x = Math.max(0, Math.floor(x0) - 1); x < Math.min(W, Math.ceil(x1) + 1); x++) {
  95. if (!inside(x, y)) continue;
  96. const t = outline - (hash(x >> 2, y >> 2) < 0.3 ? 1 : 0);
  97. const edge = outline > 0 && (!inside(x - t, y) || !inside(x + t, y) || !inside(x, y - t) || !inside(x, y + t));
  98. img[y * W + x] = edge ? INK : fill(x, y);
  99. }
  100. }
  101. };
  102. const rect = (x0: number, y0: number, x1: number, y1: number): Inside => (x, y) => x >= x0 && x < x1 && y >= y0 && y < y1;
  103. const ellipse = (cx: number, cy: number, rx: number, ry: number): Inside => (x, y) => ((x - cx) / rx) ** 2 + ((y - cy) / ry) ** 2 <= 1;
  104. const solid = (c: number) => (): number => c;
  105. // Wallpaper: cream with pink stripes and tiny dots, warmer toward the table.
  106. shape(0, 0, W, TABLE_Y, () => true, (x, y) => {
  107. const stripe = x % 96 < 30;
  108. const dot = !stripe && ((x + (Math.floor(y / 40) % 2) * 24) % 48) ** 2 / 9 + ((y % 40) - 20) ** 2 / 9 < 4;
  109. const base = stripe ? 0xffdbe3 : dot ? 0xf7c9a8 : 0xfff3dc;
  110. return mix(base, 0xf6d7b0, Math.max(0, (y - 300) / 900));
  111. }, 0);
  112. // Skirting board.
  113. shape(0, TABLE_Y - 40, W, TABLE_Y, rect(-10, TABLE_Y - 40, W + 10, TABLE_Y + 10), (x, y) => (y < TABLE_Y - 34 ? 0xffffff : 0xe8c9a0));
  114. // Window with sky, clouds and a sun.
  115. const wx0 = 1480, wy0 = 90, wx1 = 1900, wy1 = 470;
  116. shape(wx0 - 24, wy0 - 24, wx1 + 24, wy1 + 34, rect(wx0 - 24, wy0 - 24, wx1 + 24, wy1 + 34), solid(0xffffff));
  117. shape(wx0, wy0, wx1, wy1, rect(wx0, wy0, wx1, wy1), (x, y) => {
  118. const t = (y - wy0) / (wy1 - wy0);
  119. if ((x - 1820) ** 2 + (y - 160) ** 2 < 42 ** 2) return 0xffe14d;
  120. const cloud = [[1560, 200, 50], [1610, 185, 60], [1665, 205, 45], [1700, 330, 40], [1745, 318, 52], [1790, 335, 38]];
  121. for (const [cx, cy, r] of cloud) if ((x - cx) ** 2 + ((y - cy) * 1.5) ** 2 < r * r) return 0xffffff;
  122. return mix(0x8fd3ff, 0xd9f1ff, t);
  123. }, 4);
  124. shape(wx0, (wy0 + wy1) / 2 - 7, wx1, (wy0 + wy1) / 2 + 7, rect(wx0 - 5, (wy0 + wy1) / 2 - 7, wx1 + 5, (wy0 + wy1) / 2 + 7), solid(0xffffff), 3);
  125. shape((wx0 + wx1) / 2 - 7, wy0, (wx0 + wx1) / 2 + 7, wy1, rect((wx0 + wx1) / 2 - 7, wy0 - 5, (wx0 + wx1) / 2 + 7, wy1 + 5), solid(0xffffff), 3);
  126. // Curtains (pink gingham), gently waved.
  127. for (const side of [-1, 1]) {
  128. const cx = side < 0 ? wx0 - 30 : wx1 + 30;
  129. const inside: Inside = (x, y) => {
  130. if (y < wy0 - 50 || y > wy1 + 90) return false;
  131. const width = 70 + Math.sin(y / 30) * 8 + (y - wy0) * 0.06;
  132. return side < 0 ? x > cx - 40 && x < cx - 40 + width : x < cx + 40 && x > cx + 40 - width;
  133. };
  134. shape(cx - 120, wy0 - 60, cx + 120, wy1 + 100, inside, (x, y) => ((Math.floor(x / 14) + Math.floor(y / 14)) % 2 ? 0xff8fab : 0xffe3ea));
  135. }
  136. shape(wx0 - 90, wy0 - 66, wx1 + 90, wy0 - 46, rect(wx0 - 90, wy0 - 66, wx1 + 90, wy0 - 46), solid(0xc8964b), 3);
  137. // Shelf with jars and cups.
  138. const sy = 330;
  139. shape(70, sy, 600, sy + 22, rect(70, sy, 600, sy + 22), (x) => (x % 60 < 3 ? 0x9c6b3a : 0xc8964b));
  140. for (const [x, y] of [[110, sy + 22], [560, sy + 22]]) shape(x - 12, y, x + 12, y + 40, (px, py) => px >= x - 12 && px < x + 12 && py >= y && py < y + 40 - (px - x + 12), solid(0x9c6b3a), 3);
  141. const jar = (cx: number, w: number, h: number, color: number, lid: number): void => {
  142. shape(cx - w / 2, sy - h, cx + w / 2, sy, rect(cx - w / 2, sy - h, cx + w / 2, sy + 2), (x, y) => (x - cx + w / 2 < 8 ? mix(color, 0xffffff, 0.4) : y > sy - h * 0.6 ? color : mix(color, 0xffffff, 0.6)));
  143. shape(cx - w / 2 - 4, sy - h - 16, cx + w / 2 + 4, sy - h + 2, rect(cx - w / 2 - 4, sy - h - 16, cx + w / 2 + 4, sy - h + 2), solid(lid), 3);
  144. };
  145. jar(160, 70, 110, 0xd93a5b, 0xff6f91);
  146. jar(250, 60, 80, 0xf2c46d, 0x4dc4ff);
  147. jar(335, 80, 130, 0x5a3a22, 0x9be15d);
  148. // Teacup.
  149. shape(420, sy - 60, 520, sy, ellipse(470, sy - 60, 50, 60), (x, y) => (y < sy - 60 ? 0xfff6e4 : (x + y) % 40 < 6 ? 0xff8fab : 0xffffff));
  150. shape(505, sy - 55, 545, sy - 15, (x, y) => { const d = Math.hypot(x - 515, y - 35 - sy + 70); return d < 22 && d > 12 && x > 515; }, solid(0xffffff), 3);
  151. shape(400, sy - 10, 540, sy, ellipse(470, sy - 4, 70, 9), solid(0xffffff), 3);
  152. // Wall clock.
  153. shape(250, 470, 410, 630, ellipse(330, 550, 78, 78), (x, y) => {
  154. const d = Math.hypot(x - 330, y - 550);
  155. return d > 62 ? 0xff6f91 : 0xfff6e4;
  156. });
  157. shape(326, 495, 334, 552, rect(326, 495, 334, 552), solid(INK), 0);
  158. shape(330, 546, 375, 554, rect(330, 546, 375, 554), solid(INK), 0);
  159. for (let i = 0; i < 12; i++) {
  160. const a = (i / 12) * Math.PI * 2;
  161. const tx = 330 + Math.cos(a) * 52;
  162. const ty = 550 + Math.sin(a) * 52;
  163. shape(tx - 4, ty - 4, tx + 4, ty + 4, ellipse(tx, ty, 4, 4), solid(INK), 0);
  164. }
  165. // Bunting garland across the top.
  166. const sag = (x: number): number => 30 + 70 * Math.sin((x / W) * Math.PI);
  167. shape(0, 0, W, 130, (x, y) => Math.abs(y - sag(x)) < 3, solid(INK), 0);
  168. const flagColors = [0xff6f91, 0xffe14d, 0x4dc4ff, 0x9be15d, 0xb18cff];
  169. for (let i = 0, fx = 20; fx < W; i++, fx += 110) {
  170. const top = sag(fx + 35);
  171. const inside: Inside = (x, y) => {
  172. const t = (y - top) / 70;
  173. return t >= 0 && t <= 1 && Math.abs(x - (fx + 35)) <= 35 * (1 - t);
  174. };
  175. shape(fx - 2, top - 2, fx + 72, top + 74, inside, (x, y) => (hash(x >> 3, y >> 3) < 0.15 ? 0xffffff : flagColors[i % flagColors.length]));
  176. }
  177. // Table: lace edge then a gingham tablecloth with a hint of perspective.
  178. shape(0, TABLE_Y, W, H, rect(-10, TABLE_Y, W + 10, H + 10), (x, y) => {
  179. const depth = (y - TABLE_Y) / (H - TABLE_Y);
  180. const cell = 40 + depth * 30;
  181. const cx = Math.floor((x - W / 2) / cell);
  182. const cy = Math.floor((y - TABLE_Y) / (cell * 0.6));
  183. const a = (cx & 1) === 0;
  184. const b = (cy & 1) === 0;
  185. const base = a && b ? 0xe85d75 : a || b ? 0xf7a8b8 : 0xfff3f5;
  186. return mix(base, 0x7a3a2a, depth * 0.25);
  187. });
  188. shape(0, TABLE_Y - 6, W, TABLE_Y + 30, (x, y) => y >= TABLE_Y - 6 && y < TABLE_Y + 14 + 12 * Math.abs(Math.sin((x / 40) * Math.PI)), (x, y) => (((x % 40) - 20) ** 2 + (y - TABLE_Y - 8) ** 2 < 25 ? 0xf3dfe3 : 0xffffff), 3);
  189. return img;
  190. }
  191. mkdirSync(outDir, { recursive: true });
  192. writePng('terrain.png', W, H, (x, y) => terrain[y * W + x]);
  193. const background = paintBackground();
  194. writePng('background.png', W, H, (x, y) => background[y * W + x]);
  195. const TW = 256;
  196. const TH = 128;
  197. writePng('thumb.png', TW, TH, (x, y) => {
  198. const c = terrain[Math.floor((y * H) / TH) * W + Math.floor((x * W) / TW)];
  199. return c === VOID ? 0xbfe6ff : c;
  200. });
  201. console.log(`Wrote terrain.png, background.png and thumb.png to ${outDir}`);