diff --git a/.gitignore b/.gitignore index da1e380..dcdbce5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ www/dungeoncraft.wasm www/mq_js_bundle.js play/ dist/ +art/generated/creature-previews/ diff --git a/art/generated/textures/PROMPTS.md b/art/generated/textures/PROMPTS.md new file mode 100644 index 0000000..6312ca2 --- /dev/null +++ b/art/generated/textures/PROMPTS.md @@ -0,0 +1,65 @@ +# High-res tile textures (test) + +Generated on 2026-11-07 with fal.ai for the high-res graphics style (`HD_TILES` in `src/gfx.rs`). + +- Model: `fal-ai/flux-2-pro`, `image_size: square_hd` (1024x1024), `output_format: png`, + `seed: 4242` (floor and gold: a second try with `seed: 4243` and the prompts below; the first + floor had a frame and a centre motif, the first gold too few veins) +- Prompt: the kind's sentence followed by the shared style text. +- `tools/make_tile.py -raw.png --size 128 --colors 32` scales it to a 2x2 block of + 64x64 tiles, makes it seamless and reduces the colours; `-128.png` is copied to + `assets/textures/.png`. `-128-preview.png`: 3x3 copies to check the seams. + +Shared style text: + +```text +Seamless tileable square texture, viewed straight from above (orthographic top-down, no perspective), flat even lighting, no shadows, no vignette, no border, no text. Hand-painted pixel art style for a dark-fantasy dungeon strategy game: chunky clear shapes, crisp edges, limited muted palette, readable at small size. The pattern continues seamlessly across all four edges. +``` + +## floor: T_FLOOR (dug-out floor) + +```text +A uniform all-over pattern of rough grey-brown cobblestones of mixed sizes set in packed dirt, evenly spread from edge to edge with no frame, no border and no centre motif, like a cropped piece of a large dungeon floor. +``` + +## claimed: T_CLAIM (claimed floor) + +```text +The floor of an evil dungeon master's lair: fitted dark crimson-purple flagstones with thin black grout lines, some stones carry a small faint carved demonic rune, worn and polished. +``` + +## earth: T_EARTH (earth blocks) + +```text +Solid packed brown earth seen from above, soil with small stones, roots and darker clumps, slightly lumpy. +``` + +## rock: T_ROCK (bedrock) + +```text +Hard dark blue-grey bedrock seen from above, angular rock facets and deep cracks, very solid and impenetrable. +``` + +## gold: T_GOLD (gold seam) + +```text +A uniform all-over pattern of brown earth crossed by many thick glittering veins of bright yellow gold ore with large shining gold nuggets, the gold covering about a third of the surface, evenly spread from edge to edge. +``` + +## water: T_WATER + +```text +Deep dark blue underground river water seen from above, gentle wave crests and ripples in lighter blue-cyan, slight foam streaks. +``` + +## lava: T_LAVA + +```text +Molten lava seen from above, glowing orange and yellow molten rock flowing in swirls, darker red cooling streaks, very hot and bright, no black crust. +``` + +## In the game + +Water and lava flow as before (the shader moves the texture along the river and lets the surface +swell); in the high-res style a second, larger layer of the same texture drifts slower and +sideways over it, so the surface churns. diff --git a/art/generated/textures/claimed-128-preview.png b/art/generated/textures/claimed-128-preview.png new file mode 100644 index 0000000..c36e06f Binary files /dev/null and b/art/generated/textures/claimed-128-preview.png differ diff --git a/art/generated/textures/claimed-128.png b/art/generated/textures/claimed-128.png new file mode 100644 index 0000000..8a2cd2e Binary files /dev/null and b/art/generated/textures/claimed-128.png differ diff --git a/art/generated/textures/claimed-raw.png b/art/generated/textures/claimed-raw.png new file mode 100644 index 0000000..3ff18cd Binary files /dev/null and b/art/generated/textures/claimed-raw.png differ diff --git a/art/generated/textures/earth-128-preview.png b/art/generated/textures/earth-128-preview.png new file mode 100644 index 0000000..3062188 Binary files /dev/null and b/art/generated/textures/earth-128-preview.png differ diff --git a/art/generated/textures/earth-128.png b/art/generated/textures/earth-128.png new file mode 100644 index 0000000..ce671c2 Binary files /dev/null and b/art/generated/textures/earth-128.png differ diff --git a/art/generated/textures/earth-raw.png b/art/generated/textures/earth-raw.png new file mode 100644 index 0000000..9e1a6e0 Binary files /dev/null and b/art/generated/textures/earth-raw.png differ diff --git a/art/generated/textures/floor-128-preview.png b/art/generated/textures/floor-128-preview.png new file mode 100644 index 0000000..ea61880 Binary files /dev/null and b/art/generated/textures/floor-128-preview.png differ diff --git a/art/generated/textures/floor-128.png b/art/generated/textures/floor-128.png new file mode 100644 index 0000000..d8fc61b Binary files /dev/null and b/art/generated/textures/floor-128.png differ diff --git a/art/generated/textures/floor-raw.png b/art/generated/textures/floor-raw.png new file mode 100644 index 0000000..56a23dc Binary files /dev/null and b/art/generated/textures/floor-raw.png differ diff --git a/art/generated/textures/gold-128-preview.png b/art/generated/textures/gold-128-preview.png new file mode 100644 index 0000000..69791b8 Binary files /dev/null and b/art/generated/textures/gold-128-preview.png differ diff --git a/art/generated/textures/gold-128.png b/art/generated/textures/gold-128.png new file mode 100644 index 0000000..b01eb99 Binary files /dev/null and b/art/generated/textures/gold-128.png differ diff --git a/art/generated/textures/gold-raw.png b/art/generated/textures/gold-raw.png new file mode 100644 index 0000000..7c993d0 Binary files /dev/null and b/art/generated/textures/gold-raw.png differ diff --git a/art/generated/textures/lava-128-preview.png b/art/generated/textures/lava-128-preview.png new file mode 100644 index 0000000..1707c91 Binary files /dev/null and b/art/generated/textures/lava-128-preview.png differ diff --git a/art/generated/textures/lava-128.png b/art/generated/textures/lava-128.png new file mode 100644 index 0000000..3af9efd Binary files /dev/null and b/art/generated/textures/lava-128.png differ diff --git a/art/generated/textures/lava-raw.png b/art/generated/textures/lava-raw.png new file mode 100644 index 0000000..c457d7c Binary files /dev/null and b/art/generated/textures/lava-raw.png differ diff --git a/art/generated/textures/rock-128-preview.png b/art/generated/textures/rock-128-preview.png new file mode 100644 index 0000000..46b2e56 Binary files /dev/null and b/art/generated/textures/rock-128-preview.png differ diff --git a/art/generated/textures/rock-128.png b/art/generated/textures/rock-128.png new file mode 100644 index 0000000..9356781 Binary files /dev/null and b/art/generated/textures/rock-128.png differ diff --git a/art/generated/textures/rock-raw.png b/art/generated/textures/rock-raw.png new file mode 100644 index 0000000..1c3cc1c Binary files /dev/null and b/art/generated/textures/rock-raw.png differ diff --git a/art/generated/textures/water-128-preview.png b/art/generated/textures/water-128-preview.png new file mode 100644 index 0000000..b557eae Binary files /dev/null and b/art/generated/textures/water-128-preview.png differ diff --git a/art/generated/textures/water-128.png b/art/generated/textures/water-128.png new file mode 100644 index 0000000..e38db46 Binary files /dev/null and b/art/generated/textures/water-128.png differ diff --git a/art/generated/textures/water-raw.png b/art/generated/textures/water-raw.png new file mode 100644 index 0000000..2b5d7fd Binary files /dev/null and b/art/generated/textures/water-raw.png differ diff --git a/assets/textures/claimed.png b/assets/textures/claimed.png new file mode 100644 index 0000000..8a2cd2e Binary files /dev/null and b/assets/textures/claimed.png differ diff --git a/assets/textures/earth.png b/assets/textures/earth.png new file mode 100644 index 0000000..ce671c2 Binary files /dev/null and b/assets/textures/earth.png differ diff --git a/assets/textures/floor.png b/assets/textures/floor.png new file mode 100644 index 0000000..d8fc61b Binary files /dev/null and b/assets/textures/floor.png differ diff --git a/assets/textures/gold.png b/assets/textures/gold.png new file mode 100644 index 0000000..b01eb99 Binary files /dev/null and b/assets/textures/gold.png differ diff --git a/assets/textures/lava.png b/assets/textures/lava.png new file mode 100644 index 0000000..3af9efd Binary files /dev/null and b/assets/textures/lava.png differ diff --git a/assets/textures/rock.png b/assets/textures/rock.png new file mode 100644 index 0000000..9356781 Binary files /dev/null and b/assets/textures/rock.png differ diff --git a/assets/textures/water.png b/assets/textures/water.png new file mode 100644 index 0000000..e38db46 Binary files /dev/null and b/assets/textures/water.png differ diff --git a/src/gfx.rs b/src/gfx.rs index 97d9e9c..0fa89a5 100644 --- a/src/gfx.rs +++ b/src/gfx.rs @@ -99,6 +99,7 @@ pub struct Gfx { pub atlas: Texture2D, pub sprites: Texture2D, pub hd_sprites: Texture2D, // detailed sprites (see HD_SPRITES, hd_cell_view) + pub atlas_hd: Texture2D, // the tile atlas of the high-res style (see HD_TILES) pub cursors: Texture2D, // mouse pointer frames, 16x16 each (see CURSOR_*) pub badges: Texture2D, // achievement badges, 32x32 each (see badges.rs) pub font: Texture2D, // Dinowyde (glyphs of 6x8 px ordered by Unicode, see font_slot) and DINOBYTE @@ -325,6 +326,55 @@ fn earth_px(x: i32, y: i32, v: u32) -> Color { shade(base, (big + fine) as i32) } +// ---- high-res tiles +// The high-res style's atlas: the same cells as the 16x16 atlas, HD_TILE x HD_TILE texels each. +// Cells without an own texture are the 16x16 ones scaled up. HD_TILES: generated textures (see +// art/generated/textures/), each a seamless 2x2 block of tiles for the four variants of a kind +// (variant k = quarter k % 2, k / 2); the world lays the quarters out by the tiles' positions +// (see hd_tile), so a floor repeats only every two tiles and has no seams. +pub const HD_TILE: u32 = 64; +pub const ATLAS_HD_W: f32 = (ATLAS_COLS * HD_TILE) as f32; +pub const ATLAS_HD_H: f32 = (ATLAS_ROWS * HD_TILE) as f32; +const HD_TILES: [(u32, &[u8]); 7] = [ + (T_FLOOR, include_bytes!("../assets/textures/floor.png")), + (T_CLAIM, include_bytes!("../assets/textures/claimed.png")), + (T_EARTH, include_bytes!("../assets/textures/earth.png")), + (T_ROCK, include_bytes!("../assets/textures/rock.png")), + (T_GOLD, include_bytes!("../assets/textures/gold.png")), + (T_WATER, include_bytes!("../assets/textures/water.png")), + (T_LAVA, include_bytes!("../assets/textures/lava.png")), +]; + +// whether a cell has a generated high-res texture (its variants are laid out by position) +pub fn hd_tile(id: u32) -> bool { + HD_TILES.iter().any(|&(base, _)| id / 4 * 4 == base) +} + +fn gen_hd_atlas() -> Image { + let small = gen_atlas(); + let k = HD_TILE / 16; + let mut img = Image::gen_image_color(ATLAS_HD_W as u16, ATLAS_HD_H as u16, Color::new(0.0, 0.0, 0.0, 0.0)); + for y in 0..ATLAS_HD_H as u32 { + for x in 0..ATLAS_HD_W as u32 { + img.set_pixel(x, y, small.get_pixel(x / k, y / k)); + } + } + for &(base, png) in HD_TILES.iter() { + let src = Image::from_file_with_format(png, Some(ImageFormat::Png)).expect("hd tile"); + for v in 0..4 { + let (sx, sy) = ((v % 2) * HD_TILE, (v / 2) * HD_TILE); + let id = base + v; + let (ox, oy) = ((id % ATLAS_COLS) * HD_TILE, (id / ATLAS_COLS) * HD_TILE); + for y in 0..HD_TILE { + for x in 0..HD_TILE { + img.set_pixel(ox + x, oy + y, src.get_pixel(sx + x, sy + y)); + } + } + } + } + img +} + fn gen_atlas() -> Image { let mut cv = Canvas { img: Image::gen_image_color((ATLAS_COLS * 16) as u16, (ATLAS_ROWS * 16) as u16, Color::new(0.0, 0.0, 0.0, 0.0)) }; for v in 0..4u32 { @@ -2360,9 +2410,11 @@ impl Gfx { cursors.set_filter(FilterMode::Nearest); let badges = Texture2D::from_image(&crate::badges::gen_badges()); badges.set_filter(FilterMode::Nearest); + let atlas_hd = Texture2D::from_image(&gen_hd_atlas()); + atlas_hd.set_filter(FilterMode::Nearest); let hd_sprites = Texture2D::from_image(&gen_hd_sprites()); hd_sprites.set_filter(FilterMode::Nearest); - Gfx { atlas, sprites, hd_sprites, cursors, badges, font, font_map } + Gfx { atlas, sprites, hd_sprites, atlas_hd, cursors, badges, font, font_map } } pub fn cell(id: u32) -> Rect { @@ -2632,6 +2684,39 @@ impl Gfx { } } +#[cfg(test)] +mod hd_atlas_tests { + use super::*; + + #[test] + fn high_res_atlas() { + let small = gen_atlas(); + let hd = gen_hd_atlas(); + assert_eq!((hd.width as f32, hd.height as f32), (ATLAS_HD_W, ATLAS_HD_H)); + let px = |img: &Image, x: u32, y: u32| { + let p = img.get_pixel(x, y); + (p.r, p.g, p.b, p.a) + }; + // a cell without an own texture: the 16x16 one scaled up + let (cx, cy) = ((T_TREASURY % ATLAS_COLS) * 16, (T_TREASURY / ATLAS_COLS) * 16); + for (x, y) in [(0, 0), (5, 9), (15, 15)] { + assert_eq!(px(&hd, (cx + x) * 4 + 2, (cy + y) * 4 + 1), px(&small, cx + x, cy + y)); + } + // the generated ones: the quarters of their 2x2 sheet + for &(base, png) in HD_TILES.iter() { + let src = Image::from_file_with_format(png, Some(ImageFormat::Png)).unwrap(); + assert_eq!((src.width, src.height), (2 * HD_TILE as u16, 2 * HD_TILE as u16)); + for v in 0..4 { + let id = base + v; + assert!(hd_tile(id)); + let (ox, oy) = ((id % ATLAS_COLS) * HD_TILE, (id / ATLAS_COLS) * HD_TILE); + assert_eq!(px(&hd, ox + 7, oy + 40), px(&src, (v % 2) * HD_TILE + 7, (v / 2) * HD_TILE + 40)); + } + } + assert!(!hd_tile(T_TREASURY)); + } +} + #[cfg(test)] mod sprite_tests { use super::*; diff --git a/src/render3d.rs b/src/render3d.rs index a889602..c7fdba9 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -259,6 +259,19 @@ uniform vec4 LightCol[24]; uniform float LightCount; uniform highp float Time; // same precision as in the vertex shader (required for linking) uniform vec2 AtlasSize; +uniform float CellSize; // texels per atlas cell (16; high-res 64) +uniform float HdLiquid; // 1: high-res liquids (2x2 blocks of tiles, a second flowing layer) + +// a flowing liquid's texture at world position p inside the kind's cells (uv names one of them); +// high-res: the four variant cells form a 2x2 block that repeats every two tiles +vec2 liquid_uv(vec2 p) { + vec2 cell = floor(uv * AtlasSize / CellSize); + if (HdLiquid > 0.5) { + vec2 q = mod(floor(p), 2.0); + cell.x = floor(cell.x / 4.0) * 4.0 + q.x + 2.0 * q.y; + } + return (cell + fract(p)) * CellSize / AtlasSize; +} void main() { vec2 tuv = uv; @@ -276,12 +289,22 @@ void main() { float ph2 = wpos.z * 1.9 - wpos.x * 0.5 - Time * ts * 1.3; vec2 slope = vec2(cos(ph1) * 1.3 * 0.6 - cos(ph2) * 0.5 * 0.4, cos(ph1) * 0.4 * 0.6 + cos(ph2) * 1.9 * 0.4); vec2 p = wpos.xz - dir * speed * Time + slope * (lava ? 0.05 : 0.07); - vec2 cell = floor(uv * AtlasSize / 16.0) * 16.0; - tuv = (cell + fract(p) * 16.0) / AtlasSize; + tuv = liquid_uv(p); float ripple = sin(wpos.x * 5.0 + wpos.z * 3.0 + Time * ts * 1.4) * sin(wpos.z * 4.0 - wpos.x * 2.0 + Time * ts); wave = dot(slope, vec2(0.45, 0.35)) * 0.6 + ripple * 0.4; } vec4 tex = texture2D(Texture, tuv); + if (liquid && HdLiquid > 0.5) { + // a second, larger layer drifting slower and sideways: the surface churns instead of + // only sliding along + bool lava = color.a < 0.5; + vec2 dir = wnorm.xz / max(length(wnorm.xz), 0.001); + vec2 side = vec2(-dir.y, dir.x); + float speed = lava ? 0.07 : 0.15; + vec2 p2 = wpos.xz * 0.6 + vec2(0.37, 0.71) - dir * speed * Time + side * sin(Time * (lava ? 0.21 : 0.33)) * 0.3; + vec4 tex2 = texture2D(Texture, liquid_uv(p2)); + tex = vec4(mix(tex.rgb, tex2.rgb, 0.42), 1.0); + } if (tex.a < 0.5) discard; // see-through units (invisibility): every other pixel left out if (color.a < 0.37 && mod(floor(gl_FragCoord.x) + floor(gl_FragCoord.y), 2.0) < 1.0) discard; @@ -319,6 +342,8 @@ fn lit_material() -> Material { UniformDesc::new("LightCount", UniformType::Float1), UniformDesc::new("Time", UniformType::Float1), UniformDesc::new("AtlasSize", UniformType::Float2), + UniformDesc::new("CellSize", UniformType::Float1), + UniformDesc::new("HdLiquid", UniformType::Float1), ], textures: vec![], }, @@ -766,7 +791,8 @@ fn block_shell(mk: &mut MB, map: &Map, x: i32, z: i32, n: usize, fill: [u8; 4], } } -fn build_chunk(map: &Map, amb: &[f32], glow: &[f32], cx: i32, cz: i32, atlas: &Texture2D, wall_sub: usize) -> [Option; 4] { +// `hd`: the high-res atlas; its generated textures are laid out by the tiles' positions +fn build_chunk(map: &Map, amb: &[f32], glow: &[f32], cx: i32, cz: i32, atlas: &Texture2D, wall_sub: usize, hd: bool) -> [Option; 4] { let mut mb = MB::new(); let ov = MB::new(); // textured overlays (currently unused) let mut ln = MB::new(); @@ -784,6 +810,9 @@ fn build_chunk(map: &Map, amb: &[f32], glow: &[f32], cx: i32, cz: i32, atlas: &T if t.kind == Kind::Mushroom && t.res & MUSH_GROVE != 0 { tex = crate::gfx::T_MOSS + (t.var as u32 & 3); } + if hd && crate::gfx::hd_tile(tex) { + tex = tex / 4 * 4 + (x & 1) as u32 + (z & 1) as u32 * 2; + } let (ua, ub) = cell_uv(tex); let (xf, zf) = (x as f32, z as f32); if !map.shown(idx(x, z)) { @@ -1202,6 +1231,7 @@ pub struct R3d { pub pixel_rows: u32, // pixel look: the world is rendered this many rows high (0: off) target: Option<(RenderTarget, u32, u32)>, // low resolution render target lava: Vec, // lava light per tile (see lava_field) + chunks_hd: bool, // the chunks were built with the high-res atlas } // The 3D resolution setting refers to a window 1080 device pixels high: smaller windows use a @@ -1252,7 +1282,7 @@ fn chunk_sig(map: &Map, cx: i32, cz: i32) -> u64 { impl R3d { pub fn new() -> R3d { - R3d { lit: lit_material(), glow: glow_material(), opaque: opaque_material(), chunks: Vec::new(), ncx: 0, ncz: 0, outlines: true, wall_sub: 3, max_lights: MAX_LIGHTS, pixel_rows: crate::settings::PIXEL_ART_ROWS as u32, target: None, lava: Vec::new() } + R3d { lit: lit_material(), glow: glow_material(), opaque: opaque_material(), chunks: Vec::new(), ncx: 0, ncz: 0, outlines: true, wall_sub: 3, max_lights: MAX_LIGHTS, pixel_rows: crate::settings::PIXEL_ART_ROWS as u32, target: None, lava: Vec::new(), chunks_hd: false } } // Changing the wall detail rebuilds all chunks @@ -1292,6 +1322,14 @@ impl R3d { ch.sig = 0; } } + // the graphics style changed: the chunks are built again with the other atlas + let hd = self.pixel_rows >= crate::settings::HIGH_RES_ROWS as u32; + if hd != self.chunks_hd { + self.chunks_hd = hd; + for ch in self.chunks.iter_mut() { + ch.sig = 0; + } + } if view_side() != side { // another master looks: the marks change colour VIEW_SIDE.with(|v| v.set(side)); @@ -1316,7 +1354,8 @@ impl R3d { lava_fresh = true; } let ch = &mut self.chunks[(cz * self.ncx + cx) as usize]; - let [m, o, l, k] = build_chunk(&g.map, a, &self.lava, cx, cz, &gfx.atlas, self.wall_sub); + let atlas = if self.chunks_hd { &gfx.atlas_hd } else { &gfx.atlas }; + let [m, o, l, k] = build_chunk(&g.map, a, &self.lava, cx, cz, atlas, self.wall_sub, self.chunks_hd); ch.mesh = m; ch.overlay = o; ch.lines = l; @@ -1548,7 +1587,10 @@ impl R3d { self.lit.set_uniform("LightCount", count); // wrapped so the shader keeps its precision (the flow speeds are whole tiles per period) self.lit.set_uniform("Time", (time % 1000.0) as f32); - self.lit.set_uniform("AtlasSize", vec2(ATLAS_W, ATLAS_H)); + let hd_tiles = self.chunks_hd; + self.lit.set_uniform("AtlasSize", if hd_tiles { vec2(crate::gfx::ATLAS_HD_W, crate::gfx::ATLAS_HD_H) } else { vec2(ATLAS_W, ATLAS_H) }); + self.lit.set_uniform("CellSize", if hd_tiles { crate::gfx::HD_TILE as f32 } else { 16.0 }); + self.lit.set_uniform("HdLiquid", if hd_tiles { 1.0f32 } else { 0.0 }); gl_use_material(&self.lit); for cz in 0..self.ncz { for cx in 0..self.ncx { diff --git a/tools/make_tile.py b/tools/make_tile.py new file mode 100644 index 0000000..3d265cb --- /dev/null +++ b/tools/make_tile.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""Turn a generated texture into a seamless pixel-art tile sheet for the high-res atlas. + +Usage: + make_tile.py raw.png out_prefix [--size 128] [--colors 32] [--band 14] + +The picture is scaled down to size x size (default 128: a 2x2 block of 64x64 tiles; the game +lays the four quarters out by the tiles' positions, so the floor repeats only every two tiles), +made seamless (near the edges it fades into a copy shifted by half the size, whose edges wrap +around without a seam; `--band` pixels wide) and reduced to a palette without dithering. + +Writes out_prefix-.png and out_prefix--preview.png (3x3 copies scaled up 3x, to +check the seams). Needs Pillow. +""" +import argparse + +from PIL import Image, ImageChops + + +def seamless(img, band): + n = img.width + shifted = ImageChops.offset(img, n // 2, n // 2) + mask = Image.new("L", (n, n)) + for y in range(n): + for x in range(n): + # 0 at the edges (the shifted copy, continuous across the wrap), 255 inside + d = min(x, n - 1 - x, y, n - 1 - y) + mask.putpixel((x, y), int(255 * min(1.0, d / band))) + return Image.composite(img, shifted, mask) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("raw") + ap.add_argument("out") + ap.add_argument("--size", type=int, default=128) + ap.add_argument("--colors", type=int, default=32) + ap.add_argument("--band", type=int, default=14) + a = ap.parse_args() + + img = Image.open(a.raw).convert("RGB") + s = min(img.size) + img = img.crop(((img.width - s) // 2, (img.height - s) // 2, (img.width + s) // 2, (img.height + s) // 2)) + img = img.resize((a.size, a.size), Image.LANCZOS) + img = seamless(img, a.band) + img = img.quantize(colors=a.colors, method=Image.Quantize.MEDIANCUT, dither=Image.Dither.NONE).convert("RGB") + img.save(f"{a.out}-{a.size}.png") + prev = Image.new("RGB", (a.size * 3, a.size * 3)) + for k in range(9): + prev.paste(img, ((k % 3) * a.size, (k // 3) * a.size)) + prev.resize((prev.width * 2, prev.height * 2), Image.NEAREST).save(f"{a.out}-{a.size}-preview.png") + print(f"{a.out}-{a.size}.png") + + +if __name__ == "__main__": + main()