feat(editor): add --gen-texture-halftone gradient-modulated dots

80th procedural texture: regular grid of dots whose radii
grow with a configurable gradient direction. Three modes:

  • v (vertical)    — radii grow top-to-bottom
  • h (horizontal)  — radii grow left-to-right
  • r (radial)      — radii grow from texture center outward

Mimics the comic-print / newspaper image-reproduction trick
of varying dot size to encode grayscale. Distinct from
--gen-texture-dots (uniform radius across the grid) and
--gen-texture-studs (uniform with derived inner highlight)
— halftone is the gradient-modulated variant.

Useful for retro-comic / newspaper-aesthetic surfaces, vintage-
poster overlays, sci-fi monitor screens (radial mode reads as
CRT vignette), banner gradient detail, dawn/dusk sky overlays.

Default 16-stride / maxR=7 / vertical reads as classic comic
shading at 256x256.
This commit is contained in:
Kelsi 2026-05-09 14:12:04 -07:00
parent d537d7163e
commit 0a7c6c096d
3 changed files with 88 additions and 1 deletions

View file

@ -119,7 +119,7 @@ const char* const kArgRequired[] = {
"--gen-texture-embroidery", "--gen-texture-lightbeam",
"--gen-texture-dewdrops", "--gen-texture-pinwheel",
"--gen-texture-scratched-metal", "--gen-texture-crackle",
"--gen-texture-star",
"--gen-texture-star", "--gen-texture-halftone",
"--validate-glb", "--info-glb", "--info-glb-tree", "--info-glb-bytes",
"--validate-jsondbc", "--check-glb-bounds", "--validate-stl",
"--validate-png", "--validate-blp",

View file

@ -3500,6 +3500,90 @@ int handleKnit(int& i, int argc, char** argv) {
return 0;
}
int handleHalftone(int& i, int argc, char** argv) {
// Halftone: regular grid of dots whose radii grow with a
// configurable gradient direction (vertical, horizontal, or
// radial-from-center). Mimics the comic-print / newspaper
// image-reproduction trick of varying dot size to encode
// grayscale. Distinct from --gen-texture-dots (uniform dot
// radius) and --gen-texture-studs (uniform with inner
// highlight) — halftone is the gradient-modulated variant.
std::string outPath = argv[++i];
std::string bgHex = argv[++i];
std::string dotHex = argv[++i];
int stride = 16;
int maxR = 7;
char dir = 'v'; // 'v' vertical, 'h' horizontal, 'r' radial
int W = 256, H = 256;
parseOptInt(i, argc, argv, stride);
parseOptInt(i, argc, argv, maxR);
if (i + 1 < argc && argv[i + 1][0] != '-') {
const char* a = argv[++i];
if (a[0] == 'h' || a[0] == 'H') dir = 'h';
else if (a[0] == 'r' || a[0] == 'R') dir = 'r';
else dir = 'v';
}
parseOptInt(i, argc, argv, W);
parseOptInt(i, argc, argv, H);
if (W < 1 || H < 1 || W > 8192 || H > 8192 ||
stride < 4 || stride > 1024 ||
maxR < 1 || maxR * 2 >= stride) {
std::fprintf(stderr,
"gen-texture-halftone: invalid dims (W/H 1..8192, "
"stride 4..1024, maxR 1..stride/2)\n");
return 1;
}
uint8_t br_, bg_, bb_, dr, dg, db;
if (!parseHexOrError(bgHex, br_, bg_, bb_,
"gen-texture-halftone")) return 1;
if (!parseHexOrError(dotHex, dr, dg, db,
"gen-texture-halftone")) return 1;
std::vector<uint8_t> pixels(static_cast<size_t>(W) * H * 3, 0);
const float cx = W * 0.5f;
const float cy = H * 0.5f;
const float maxDist = std::sqrt(cx * cx + cy * cy);
for (int y = 0; y < H; ++y) {
for (int x = 0; x < W; ++x) {
// Find nearest grid center.
int col = (x + stride / 2) / stride;
int row = (y + stride / 2) / stride;
float gx = col * stride;
float gy = row * stride;
// Gradient value t in [0, 1] at the cell center based
// on direction.
float t;
if (dir == 'h') {
t = gx / (W - 1);
} else if (dir == 'r') {
float dxc = gx - cx, dyc = gy - cy;
t = std::sqrt(dxc * dxc + dyc * dyc) / maxDist;
} else {
t = gy / (H - 1);
}
float r = maxR * t;
float ddx = x - gx;
float ddy = y - gy;
float d = std::sqrt(ddx * ddx + ddy * ddy);
uint8_t outR, outG, outB;
if (d < r) {
outR = dr; outG = dg; outB = db;
} else {
outR = br_; outG = bg_; outB = bb_;
}
setPixelRGB(pixels, W, x, y, outR, outG, outB);
}
}
if (!savePngOrError(outPath, W, H, pixels,
"gen-texture-halftone")) return 1;
printPngWrote(outPath, W, H);
std::printf(" bg/dot : %s / %s\n", bgHex.c_str(), dotHex.c_str());
std::printf(" grid : stride=%d, maxR=%d, dir=%s\n",
stride, maxR,
dir == 'h' ? "horizontal" :
(dir == 'r' ? "radial" : "vertical"));
return 0;
}
int handleStar(int& i, int argc, char** argv) {
// N-pointed star polygon centered on the texture. Each pixel
// computes its polar (r, θ); the star boundary at any θ
@ -5831,6 +5915,7 @@ constexpr TextureEntry kTextureTable[] = {
{"--gen-texture-scratched-metal",3, handleScratchedMetal},
{"--gen-texture-crackle", 3, handleCrackle},
{"--gen-texture-star", 3, handleStar},
{"--gen-texture-halftone", 3, handleHalftone},
};
} // namespace

View file

@ -193,6 +193,8 @@ void printUsage(const char* argv0) {
std::printf(" Crackle: fine Voronoi cell-boundary cracks (dried mud / parched earth / aged leather)\n");
std::printf(" --gen-texture-star <out.png> <bgHex> <starHex> [points] [innerFrac] [W H]\n");
std::printf(" Star: solid N-pointed star polygon centered (medallions / shields / religious symbols)\n");
std::printf(" --gen-texture-halftone <out.png> <bgHex> <dotHex> [stride] [maxR] [v|h|r] [W H]\n");
std::printf(" Halftone: grid of dots whose radii grow with a v/h/r gradient (comic / newspaper print look)\n");
std::printf(" --add-texture-to-zone <zoneDir> <png-path> [renameTo]\n");
std::printf(" Copy an existing PNG into <zoneDir> (optionally renaming it on the way in)\n");
std::printf(" --gen-mesh <wom-base> <cube|plane|sphere|cylinder|torus|cone|ramp> [size]\n");