Something went wrong. Try again.
Web frontend and supporting services for lance.blue
Something went wrong. Try again.
3.6 kB · 87 lines
TypeScript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788/** * Rolling a camo nobody designed, and rasterising one. * * Two callers want a camo out of thin air: the editor, which rolls one on * every page load and again on Randomize, and the challenge screen, which * rolls one for the bot at every launch. They want different things out of it * — the editor wants the settings so the sliders can carry on from there, the * launcher wants the picture — so both are here rather than the roll being * written down twice with two sets of ranges that drift apart. */
import { CAMO_H, CAMO_W } from "./mech";import { GENERATORS, byId, type PatternParams } from "./patterns";import { PRESETS, generatePalette, type Palette } from "./palette";import { randomSeed } from "./rng";import { el } from "../dom";
export type RolledCamo = { generator: string; params: PatternParams; palette: Palette;};
/** * A whole camo rolled: pattern, seed, palette and shape. Landing on the same * woodland blobs every time makes the page look like it only does one thing, * when the point is that it does twenty. * * Solid and Pride sit out of the draw. Solid is a flat colour, which is a fine * thing to choose and a dull thing to be handed; Pride only means anything * with a flag palette, and a flag is something a person picks deliberately. */export function randomCamo(): RolledCamo { const pool = GENERATORS.filter((g) => g.id !== "solid" && g.id !== "pride"); const gen = pool[Math.floor(Math.random() * pool.length)]!; // Hand-made presets are trusted as-is and deliberately not held to the // separation rule that generated palettes are. Real camouflage is tonal: // Woodland's closest pair is 29 apart and Jungle's is 37, well under the // threshold, and both are good-looking camo rather than the accidental // four-shades-of-mud that the rule exists to catch. const schemes = PRESETS.filter((p) => !p.flag); const palette = Math.random() < 0.5 ? schemes[Math.floor(Math.random() * schemes.length)]! : generatePalette(randomSeed()); return { generator: gen.id, palette, params: { seed: randomSeed(), // Kept off the extremes: the ends of these ranges are useful when you // are reaching for them and rarely flattering as an opening hand. scale: 0.25 + Math.random() * 0.5, contrast: 0.4 + Math.random() * 0.5, detail: 0.2 + Math.random() * 0.6, angle: Math.random() < 0.5 ? 0 : Math.random(), }, };}
/** * A rolled camo as the 84x72 PNG, which is the only form anything outside * this directory has a use for. * * Separate from the roll itself because a caller that means to *keep* the * camo needs both halves: the picture to upload, and the settings that made * it, so the saved record can be re-edited rather than being a flat image * nobody can get back inside. A bot's camo needs only the picture, which is * what `rollCamoPng` below is for. */export function renderCamoPng(rolled: RolledCamo): Promise<Blob> { const image = new ImageData(CAMO_W, CAMO_H); byId(rolled.generator).render(image, rolled.palette.colors, rolled.params); const canvas = el("canvas", { width: CAMO_W, height: CAMO_H }); canvas.getContext("2d")!.putImageData(image, 0, 0); return new Promise((resolve, reject) => { canvas.toBlob((blob) => { if (blob) resolve(blob); else reject(new Error("The camo could not be rasterised.")); }, "image/png"); });}
/** Roll one and hand back only the picture. */export function rollCamoPng(): Promise<Blob> { return renderCamoPng(randomCamo());}