Imp digging animation and gold sack in high-res; sack also on the small imp instead of the coin sign
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No files matched your search
@@ -34,15 +34,77 @@ reduces them to one shared palette:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 tools/pixelize_sprite.py raw-1.png imp --views 5 --size 32 --colors 16 --crop 0,0.27,1,0.72
|
python3 tools/pixelize_sprite.py raw-1.png imp --views 5 --size 32 --colors 16 --crop 0,0.27,1,0.72
|
||||||
python3 tools/pixelize_sprite.py raw-1.png imp --views 5 --size 24 --colors 16 --crop 0,0.27,1,0.72
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Results: `imp-32.png` / `imp-24.png` (views front, front 3/4, side, back 3/4, back; transparent)
|
Result: `imp-32.png` (views front, front 3/4, side, back 3/4, back; transparent) and the 8x
|
||||||
and the 8x previews `imp-32-preview.png` / `imp-24-preview.png`.
|
preview `imp-32-preview.png` (a 24x24 test lost most of the face and was dropped). The final
|
||||||
|
idle sheet was made again with the body scale of the animation (see below).
|
||||||
|
|
||||||
## Findings
|
## Findings
|
||||||
|
|
||||||
- 32x32 keeps the face (eyes, grin) and the silhouette readable; 24x24 already loses most of the face.
|
- 32x32 keeps the face (eyes, grin) and the silhouette readable; 24x24 already loses most of the face.
|
||||||
- The pickaxe's iron mostly turns dark; small details would need a manual touch-up.
|
- The pickaxe's iron mostly turns dark; small details would need a manual touch-up.
|
||||||
- The sprites are not used by the game yet: the sprite atlas (`gen_sprites` in `src/gfx.rs`) only
|
- In the high-res graphics style the game uses `imp-32.png` (idle) and `imp-hack-32.png` (digging).
|
||||||
knows 10x10 sprites in 12x12 cells.
|
|
||||||
|
## Digging animation (2026-11-07)
|
||||||
|
|
||||||
|
Four frames per view (wind-up, swing, impact, recover), made with `fal-ai/nano-banana-pro/edit`
|
||||||
|
(`resolution: 2K`, `num_images: 2`, `output_format: png`).
|
||||||
|
|
||||||
|
1. Front, front three-quarter and side: reference image = the lower row of `raw-1.png` (scaled
|
||||||
|
to half size), `aspect_ratio: 4:5`. Used: `hack-raw-1.png`, rows 1-3 (its rows 4-5 showed
|
||||||
|
the front instead of the back in some frames; the second result was dropped).
|
||||||
|
|
||||||
|
```text
|
||||||
|
The attached image is a pixel art sprite sheet of the "Pit Imp" in five views (left to right: front, front three-quarter turned right, side facing right, back three-quarter turned away to the right, back).
|
||||||
|
|
||||||
|
Create a new pixel art sprite sheet of exactly the same character - same proportions, colours, outline, pixel size, tunic, horns, ears, tail and the same small iron pickaxe - showing a looping digging animation: the imp swings the pickaxe with both hands and hacks at a rock wall in front of it (the wall itself is not shown).
|
||||||
|
|
||||||
|
Layout: a grid of 5 rows and 4 columns, every figure the same size, evenly spaced, each in its own equal cell, all standing on the same baseline within their row.
|
||||||
|
Rows (one view per row, top to bottom): 1 front view facing the viewer, 2 front three-quarter view turned to the right, 3 side view facing right, 4 back three-quarter view turned away to the right, 5 back view.
|
||||||
|
Columns (animation frames, left to right): 1 wind-up - the pickaxe raised high above and behind the head, body leaning back; 2 swing - the pickaxe coming down over the head, body leaning forward; 3 impact - the pickaxe head struck down low in front of the body at knee height, body bent forward, a few small rock chips flying; 4 recover - pulling the pickaxe back up at chest height, body upright.
|
||||||
|
The feet stay in place in all four frames; only arms, pickaxe, head and upper body move.
|
||||||
|
|
||||||
|
Style: identical to the attached sheet - crisp retro pixel art, hard square pixels, one-pixel near-black outline, limited palette, no anti-aliasing, no blur. Flat pure magenta background (#FF00FF) everywhere, no ground, no shadows, no text, no labels, no frame lines, no grid lines.
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Back three-quarter and back: reference images = the idle sheet and the upper three rows of
|
||||||
|
`hack-raw-1.png`, `aspect_ratio: 16:9`. Used: `hack-back-raw-2.png` (the first result
|
||||||
|
was dropped).
|
||||||
|
|
||||||
|
```text
|
||||||
|
The first attached image is a pixel art sprite sheet of the "Pit Imp" in five idle views (front, front three-quarter, side, back three-quarter, back). The second attached image shows the same imp's digging animation (columns: 1 wind-up with the pickaxe raised high above and behind the head, 2 swing with the pickaxe coming down over the head, 3 impact with the pickaxe struck down low in front of the body with rock chips, 4 recover with the pickaxe pulled back up at chest height) for the front, front three-quarter and side views.
|
||||||
|
|
||||||
|
Create the same four-frame digging animation for the two views seen FROM BEHIND, where the imp faces away from the viewer and we see the back of its head, its back, the tunic from behind and the tail; the face and eyes are never visible:
|
||||||
|
Row 1: back three-quarter view, the imp turned away from the viewer towards the upper right, hacking at a wall in front of it (away from the viewer).
|
||||||
|
Row 2: back view, the imp seen straight from behind, hacking at a wall in front of it (away from the viewer).
|
||||||
|
Columns (frames, left to right): 1 wind-up - pickaxe raised high above the head, its head visible above the imp's head; 2 swing - pickaxe coming down, partly hidden by the head; 3 impact - the pickaxe is in front of the imp and mostly hidden by its body, only the tip shows low at one side, a few rock chips fly up beside the body; 4 recover - pickaxe pulled back up at shoulder height, its handle visible beside the body.
|
||||||
|
The feet stay in place in all frames.
|
||||||
|
|
||||||
|
Layout: a grid of exactly 2 rows and 4 columns, every figure the same size and the same character size as in the attached sheets, evenly spaced, each in its own equal cell, all standing on the same baseline.
|
||||||
|
Style: identical to the attached sheets - same character, proportions, colours, outline and pixel size, crisp retro pixel art, hard square pixels, no anti-aliasing, no blur. Flat pure magenta background (#FF00FF) everywhere, no ground, no shadows, no text, no labels, no frame lines, no grid lines.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pixel grid
|
||||||
|
|
||||||
|
All sheets of the imp share one body scale (horns to feet = 30 pixels, measured with
|
||||||
|
`--measure` on a frame where the pickaxe is below the horns), the feet row 31 and the idle
|
||||||
|
sheet's palette plus two iron greys:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
T=tools/pixelize_sprite.py
|
||||||
|
python3 $T raw-1.png imp --views 5 --size 32 --colors 16 --crop 0,0.27,1,0.72 --step 18.43 --feet 31
|
||||||
|
# per view row of the hack sheets (body heights 438, 441, 399 / 664, 653 source pixels):
|
||||||
|
python3 $T hack-raw-1.png row0 --grid 1x4 --size 32 --crop 0,0,1,0.2 --step 14.6 --feet 31 \
|
||||||
|
--palette imp-32.png --add-colors "150,152,168;104,106,122" --outline-weight 0.6
|
||||||
|
# ... rows 1-2 of hack-raw-1.png, rows 0-1 of hack-back-raw-2.png likewise,
|
||||||
|
# then the five rows stacked into imp-hack-32.png (4 frames x 5 views)
|
||||||
|
```
|
||||||
|
|
||||||
|
`imp-hack-32-preview.png`: the frames, the idle views in the right column for comparison.
|
||||||
|
The game copies `imp-32.png` and `imp-hack-32.png` to `assets/sprites/`.
|
||||||
|
|
||||||
|
### Sack
|
||||||
|
|
||||||
|
The gold sack is not generated: it is a small hand-made pixel pattern (`SACK_HD`, `SACK_SMALL`
|
||||||
|
in `src/gfx.rs`) that the game places on every frame when the atlas is built.
|
||||||
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 4.1 MiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
@@ -1374,10 +1374,43 @@ const SPR_K: u32 = 3;
|
|||||||
pub const ACELL: u32 = SPR_CELL * SPR_K;
|
pub const ACELL: u32 = SPR_CELL * SPR_K;
|
||||||
|
|
||||||
// Detailed 32x32 sprites (generated, see art/generated/), used instead of the 10x10 ones in the
|
// Detailed 32x32 sprites (generated, see art/generated/), used instead of the 10x10 ones in the
|
||||||
// high-res graphics style when close enough (render3d::billboards). Five views side by side like VIEWS,
|
// high-res graphics style when close enough (render3d::billboards). Transparent background, the
|
||||||
// transparent background, the feet on the bottom row. No gear yet: every tier looks the same.
|
// feet on the bottom row. `idle`: the five views side by side (VIEWS); `hack`: the digging
|
||||||
const HD_SPRITES: [(EK, &[u8]); 1] = [(EK::Worker, include_bytes!("../assets/sprites/imp-32.png"))];
|
// animation, HACK_FRAMES frames side by side, one row per view. No gear yet.
|
||||||
|
struct HdSprite {
|
||||||
|
kind: EK,
|
||||||
|
idle: &'static [u8],
|
||||||
|
hack: &'static [u8],
|
||||||
|
}
|
||||||
|
const HD_SPRITES: [HdSprite; 1] = [HdSprite {
|
||||||
|
kind: EK::Worker,
|
||||||
|
idle: include_bytes!("../assets/sprites/imp-32.png"),
|
||||||
|
hack: include_bytes!("../assets/sprites/imp-hack-32.png"),
|
||||||
|
}];
|
||||||
const HD: u32 = 32;
|
const HD: u32 = 32;
|
||||||
|
pub const HACK_FRAMES: u32 = 4;
|
||||||
|
// atlas columns of a detailed sprite: idle and the hack frames, each without and with the sack
|
||||||
|
const HD_COLS: u32 = (1 + HACK_FRAMES) * 2;
|
||||||
|
|
||||||
|
// kinds that carry treasure in a sack on the back (instead of the coin sign over the head);
|
||||||
|
// their 10x10 sprite with the sack gets an own atlas column after SPRITE_KINDS
|
||||||
|
const SACK_KINDS: [EK; 1] = [EK::Worker];
|
||||||
|
// the sack (k outline, B burlap, b its shade and the rope, y/Y gold coins at the opening)
|
||||||
|
const SACK_SMALL: [&str; 5] = [".kk.", "kyYk", "kBbk", "kBBk", ".kk."];
|
||||||
|
const SACK_HD: [&str; 12] = [
|
||||||
|
"....kkkk....",
|
||||||
|
"...kyYYyk...",
|
||||||
|
"..kyYyyYyk..",
|
||||||
|
"..kkbbbbkk..",
|
||||||
|
"...kBBBBk...",
|
||||||
|
"..kBBBBBBk..",
|
||||||
|
".kBBBbBBBBk.",
|
||||||
|
".kBBBBBBbBk.",
|
||||||
|
".kBbBBBBBBk.",
|
||||||
|
".kBBBBBBBBk.",
|
||||||
|
"..kBBBBBBk..",
|
||||||
|
"...kkkkkk...",
|
||||||
|
];
|
||||||
|
|
||||||
fn sprite_rows(k: EK) -> [&'static str; 10] {
|
fn sprite_rows(k: EK) -> [&'static str; 10] {
|
||||||
match k {
|
match k {
|
||||||
@@ -1835,7 +1868,7 @@ pub const SPRITE_KINDS: [EK; 31] = [
|
|||||||
EK::Inquisitor,
|
EK::Inquisitor,
|
||||||
];
|
];
|
||||||
// width of the sprite texture in pixels
|
// width of the sprite texture in pixels
|
||||||
pub const SPRITE_TEX_W: f32 = ((SPRITE_KINDS.len() + HD_SPRITES.len()) as u32 * ACELL) as f32;
|
pub const SPRITE_TEX_W: f32 = ((SPRITE_KINDS.len() + SACK_KINDS.len()) as u32 * ACELL + HD_SPRITES.len() as u32 * HD_COLS * ACELL) as f32;
|
||||||
pub const SPRITE_TEX_H: f32 = (2 * VIEWS * GEAR_TIERS * ACELL) as f32;
|
pub const SPRITE_TEX_H: f32 = (2 * VIEWS * GEAR_TIERS * ACELL) as f32;
|
||||||
|
|
||||||
pub fn sprite_index(k: EK) -> u32 {
|
pub fn sprite_index(k: EK) -> u32 {
|
||||||
@@ -2031,46 +2064,124 @@ fn sprite_with_gear(k: EK, tier: u32, view: u32) -> [[Option<Color>; 12]; 12] {
|
|||||||
g
|
g
|
||||||
}
|
}
|
||||||
|
|
||||||
// row (view * GEAR_TIERS + tier) * 2: sprites with the gear of the tier, the row after it:
|
type Cell = Vec<Vec<Option<Color>>>;
|
||||||
// white silhouettes (hit flash). Columns: SPRITE_KINDS, then HD_SPRITES.
|
|
||||||
fn gen_sprites() -> Image {
|
// a sack on the back of a figure in a cell: from the front it shows beside the body (drawn
|
||||||
let n = SPRITE_KINDS.len() as u32;
|
// behind the figure), from behind on the back (in front of it); `view` as in VIEWS; `front_dx`:
|
||||||
let mut img = Image::gen_image_color(SPRITE_TEX_W as u16, SPRITE_TEX_H as u16, Color::new(0.0, 0.0, 0.0, 0.0));
|
// where it shows beside the body in the front and front three-quarter view (in body widths,
|
||||||
let put = |img: &mut Image, px: u32, py: u32, col: Color| {
|
// the side away from the tool)
|
||||||
img.set_pixel(px, py, col);
|
fn add_sack(g: &mut Cell, view: u32, pat: &[&str], front_dx: (f32, f32)) {
|
||||||
img.set_pixel(px, py + ACELL, WHITE);
|
let n = g.len();
|
||||||
|
let rows: Vec<usize> = (0..n).filter(|&y| g[y].iter().any(|p| p.is_some())).collect();
|
||||||
|
let (Some(&top), Some(&bot)) = (rows.first(), rows.last()) else { return };
|
||||||
|
let h = (bot - top + 1) as f32;
|
||||||
|
// the body's middle: the opaque columns of the torso band (tools and tails stick out)
|
||||||
|
let mut xs: Vec<usize> = Vec::new();
|
||||||
|
for y in top + (h * 0.4) as usize..=top + (h * 0.6) as usize {
|
||||||
|
xs.extend((0..n).filter(|&x| g[y][x].is_some()));
|
||||||
|
}
|
||||||
|
if xs.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
xs.sort();
|
||||||
|
let cx = xs[xs.len() / 2] as f32;
|
||||||
|
let span = (xs[xs.len() * 9 / 10] - xs[xs.len() / 10]) as f32;
|
||||||
|
let pw = pat[0].len() as f32;
|
||||||
|
// (offset of the sack's centre from the body's middle in body widths, its top in heights, in front)
|
||||||
|
let (dx, dy, front) = match view {
|
||||||
|
0 => (front_dx.0, 0.34, false),
|
||||||
|
1 => (front_dx.1, 0.32, false),
|
||||||
|
2 => (-0.55, 0.3, false),
|
||||||
|
3 => (-0.15, 0.36, true),
|
||||||
|
_ => (0.0, 0.36, true),
|
||||||
};
|
};
|
||||||
for (i, &k) in SPRITE_KINDS.iter().enumerate() {
|
let x0 = (cx + dx * span - pw * 0.5).round() as i32;
|
||||||
for view in 0..VIEWS {
|
let y0 = (top as f32 + dy * h).round() as i32;
|
||||||
for tier in 0..GEAR_TIERS {
|
for (py, row) in pat.iter().enumerate() {
|
||||||
let g = sprite_with_gear(k, tier, view);
|
for (px, ch) in row.chars().enumerate() {
|
||||||
for (y, row) in g.iter().enumerate() {
|
let col = match ch {
|
||||||
for (x, px) in row.iter().enumerate() {
|
'k' => c(22, 16, 22),
|
||||||
if let Some(col) = px {
|
'B' => c(150, 104, 58),
|
||||||
let (px, py) = (i as u32 * ACELL + x as u32 * SPR_K, (view * GEAR_TIERS + tier) * 2 * ACELL + y as u32 * SPR_K);
|
'b' => c(108, 70, 40),
|
||||||
for d in 0..SPR_K * SPR_K {
|
'y' => c(232, 176, 48),
|
||||||
put(&mut img, px + d % SPR_K, py + d / SPR_K, *col);
|
'Y' => c(255, 232, 120),
|
||||||
}
|
_ => continue,
|
||||||
}
|
};
|
||||||
}
|
let (x, y) = (x0 + px as i32, y0 + py as i32);
|
||||||
|
if x < 0 || y < 0 || x as usize >= n || y as usize >= n {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let p = &mut g[y as usize][x as usize];
|
||||||
|
if front || p.is_none() {
|
||||||
|
*p = Some(col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writes a cell into the atlas: column, cell row (the white silhouette goes into the row below),
|
||||||
|
// every cell pixel as k x k texels
|
||||||
|
fn put_cell(img: &mut Image, col: u32, row: u32, g: &Cell, k: u32) {
|
||||||
|
for (y, line) in g.iter().enumerate() {
|
||||||
|
for (x, p) in line.iter().enumerate() {
|
||||||
|
let Some(c) = p else { continue };
|
||||||
|
for d in 0..k * k {
|
||||||
|
let (px, py) = (col * ACELL + x as u32 * k + d % k, row * ACELL + y as u32 * k + d / k);
|
||||||
|
img.set_pixel(px, py, *c);
|
||||||
|
img.set_pixel(px, py + ACELL, WHITE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// a 32x32 picture from a sheet into a 36x36 cell (the feet on the 10x10 sprite's bottom row)
|
||||||
|
fn hd_cell(src: &Image, sx: u32, sy: u32) -> Cell {
|
||||||
|
let mut g = vec![vec![None; ACELL as usize]; ACELL as usize];
|
||||||
|
for y in 0..HD {
|
||||||
|
for x in 0..HD {
|
||||||
|
if sx + x < src.width as u32 && sy + y < src.height as u32 {
|
||||||
|
let p = src.get_pixel(sx + x, sy + y);
|
||||||
|
if p.a > 0.5 {
|
||||||
|
g[1 + y as usize][2 + x as usize] = Some(Color::new(p.r, p.g, p.b, 1.0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (j, (_, png)) in HD_SPRITES.iter().enumerate() {
|
g
|
||||||
let src = Image::from_file_with_format(png, Some(ImageFormat::Png)).expect("hd sprite");
|
}
|
||||||
|
|
||||||
|
// The sprite atlas. Columns: SPRITE_KINDS, SACK_KINDS (with the sack), then HD_COLS per
|
||||||
|
// detailed sprite. 10x10 sprites: row (view * GEAR_TIERS + tier) * 2 with the gear of the tier,
|
||||||
|
// the row after it the white silhouette (hit flash). Detailed sprites: row view * 2 (+1 white).
|
||||||
|
fn gen_sprites() -> Image {
|
||||||
|
let n = SPRITE_KINDS.len() as u32;
|
||||||
|
let mut img = Image::gen_image_color(SPRITE_TEX_W as u16, SPRITE_TEX_H as u16, Color::new(0.0, 0.0, 0.0, 0.0));
|
||||||
|
let small = |k: EK, tier: u32, view: u32| -> Cell { sprite_with_gear(k, tier, view).iter().map(|r| r.to_vec()).collect() };
|
||||||
|
for view in 0..VIEWS {
|
||||||
|
for tier in 0..GEAR_TIERS {
|
||||||
|
let row = (view * GEAR_TIERS + tier) * 2;
|
||||||
|
for (i, &k) in SPRITE_KINDS.iter().enumerate() {
|
||||||
|
put_cell(&mut img, i as u32, row, &small(k, tier, view), SPR_K);
|
||||||
|
}
|
||||||
|
for (i, &k) in SACK_KINDS.iter().enumerate() {
|
||||||
|
let mut g = small(k, tier, view);
|
||||||
|
add_sack(&mut g, view, &SACK_SMALL, (-0.5, -0.5));
|
||||||
|
put_cell(&mut img, n + i as u32, row, &g, SPR_K);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let base = n + SACK_KINDS.len() as u32;
|
||||||
|
for (j, hd) in HD_SPRITES.iter().enumerate() {
|
||||||
|
let idle = Image::from_file_with_format(hd.idle, Some(ImageFormat::Png)).expect("hd sprite");
|
||||||
|
let hack = Image::from_file_with_format(hd.hack, Some(ImageFormat::Png)).expect("hd sprite");
|
||||||
for view in 0..VIEWS {
|
for view in 0..VIEWS {
|
||||||
for tier in 0..GEAR_TIERS {
|
for f in 0..1 + HACK_FRAMES {
|
||||||
for y in 0..HD.min(src.height as u32) {
|
let g = if f == 0 { hd_cell(&idle, view * HD, 0) } else { hd_cell(&hack, (f - 1) * HD, view * HD) };
|
||||||
for x in 0..HD {
|
let col = base + j as u32 * HD_COLS + f * 2;
|
||||||
let col = src.get_pixel(view * HD + x, y);
|
put_cell(&mut img, col, view * 2, &g, 1);
|
||||||
if col.a > 0.5 {
|
let mut s = g;
|
||||||
// the feet on the 10x10 sprite's bottom row (cell texel 33)
|
add_sack(&mut s, view, &SACK_HD, (0.7, -0.65));
|
||||||
let (px, py) = ((n + j as u32) * ACELL + 2 + x, (view * GEAR_TIERS + tier) * 2 * ACELL + 1 + y);
|
put_cell(&mut img, col + 1, view * 2, &s, 1);
|
||||||
put(&mut img, px, py, Color::new(col.r, col.g, col.b, 1.0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2224,10 +2335,26 @@ impl Gfx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// the cell of a detailed sprite's view, if the kind has one (same layout as sprite_cell_view)
|
// the cell of a detailed sprite's view, if the kind has one (same layout as sprite_cell_view)
|
||||||
pub fn hd_cell_view(k: EK, white: bool, level: u32, view: u32) -> Option<Rect> {
|
// the cell of a detailed sprite, if the kind has one: a view, standing (`hack` None) or a
|
||||||
let j = HD_SPRITES.iter().position(|(h, _)| *h == k)? as u32;
|
// frame of the digging animation, with or without the sack
|
||||||
let row = (view.min(VIEWS - 1) * GEAR_TIERS + gear_tier(level)) * 2 + white as u32;
|
pub fn hd_cell_view(k: EK, white: bool, view: u32, hack: Option<u32>, sack: bool) -> Option<Rect> {
|
||||||
Some(Rect::new(((SPRITE_KINDS.len() as u32 + j) * ACELL) as f32, (row * ACELL) as f32, ACELL as f32, ACELL as f32))
|
let j = HD_SPRITES.iter().position(|h| h.kind == k)? as u32;
|
||||||
|
let f = hack.map_or(0, |f| 1 + f % HACK_FRAMES);
|
||||||
|
let col = (SPRITE_KINDS.len() + SACK_KINDS.len()) as u32 + j * HD_COLS + f * 2 + sack as u32;
|
||||||
|
let row = view.min(VIEWS - 1) * 2 + white as u32;
|
||||||
|
Some(Rect::new((col * ACELL) as f32, (row * ACELL) as f32, ACELL as f32, ACELL as f32))
|
||||||
|
}
|
||||||
|
|
||||||
|
// whether a kind carries its treasure in a sack on the back
|
||||||
|
pub fn has_sack(k: EK) -> bool {
|
||||||
|
SACK_KINDS.contains(&k)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the cell of a 10x10 sprite with the sack on the back (only SACK_KINDS)
|
||||||
|
pub fn sprite_cell_sack(k: EK, white: bool, level: u32, view: u32) -> Option<Rect> {
|
||||||
|
let i = SACK_KINDS.iter().position(|&s| s == k)? as u32;
|
||||||
|
let r = Gfx::sprite_cell_view(k, white, level, view);
|
||||||
|
Some(Rect::new(((SPRITE_KINDS.len() as u32 + i) * ACELL) as f32, r.y, r.w, r.h))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tile_tex(kind: Kind, owner: Owner, var: u8, reinforced: bool) -> u32 {
|
pub fn tile_tex(kind: Kind, owner: Owner, var: u8, reinforced: bool) -> u32 {
|
||||||
@@ -2454,19 +2581,37 @@ impl Gfx {
|
|||||||
mod sprite_tests {
|
mod sprite_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn opaque(img: &Image, r: Rect) -> usize {
|
||||||
|
(0..ACELL * ACELL).filter(|i| img.get_pixel(r.x as u32 + i % ACELL, r.y as u32 + i / ACELL).a > 0.5).count()
|
||||||
|
}
|
||||||
|
|
||||||
|
// texels that differ between two cells
|
||||||
|
fn differ(img: &Image, a: Rect, b: Rect) -> usize {
|
||||||
|
(0..ACELL * ACELL)
|
||||||
|
.filter(|i| {
|
||||||
|
let p = img.get_pixel(a.x as u32 + i % ACELL, a.y as u32 + i / ACELL);
|
||||||
|
let q = img.get_pixel(b.x as u32 + i % ACELL, b.y as u32 + i / ACELL);
|
||||||
|
(p.r, p.g, p.b, p.a) != (q.r, q.g, q.b, q.a)
|
||||||
|
})
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn detailed_sprites_in_the_atlas() {
|
fn detailed_sprites_in_the_atlas() {
|
||||||
let img = gen_sprites();
|
let img = gen_sprites();
|
||||||
assert_eq!((img.width as f32, img.height as f32), (SPRITE_TEX_W, SPRITE_TEX_H));
|
assert_eq!((img.width as f32, img.height as f32), (SPRITE_TEX_W, SPRITE_TEX_H));
|
||||||
for view in 0..VIEWS {
|
for view in 0..VIEWS {
|
||||||
let r = Gfx::hd_cell_view(EK::Worker, false, 1, view).expect("the imp has a detailed sprite");
|
for hack in [None, Some(0), Some(1), Some(2), Some(3)] {
|
||||||
let w = Gfx::hd_cell_view(EK::Worker, true, 1, view).unwrap();
|
let r = Gfx::hd_cell_view(EK::Worker, false, view, hack, false).expect("the imp has a detailed sprite");
|
||||||
let opaque = |r: Rect| (0..ACELL * ACELL).filter(|i| img.get_pixel(r.x as u32 + i % ACELL, r.y as u32 + i / ACELL).a > 0.5).count();
|
let w = Gfx::hd_cell_view(EK::Worker, true, view, hack, false).unwrap();
|
||||||
assert!(opaque(r) > 150, "view {view} drawn");
|
let s = Gfx::hd_cell_view(EK::Worker, false, view, hack, true).unwrap();
|
||||||
assert_eq!(opaque(r), opaque(w), "white silhouette matches");
|
assert!(opaque(&img, r) > 150, "view {view} {hack:?} drawn");
|
||||||
|
assert_eq!(opaque(&img, r), opaque(&img, w), "white silhouette matches");
|
||||||
|
assert!(differ(&img, s, r) > 15, "view {view} {hack:?}: the sack shows");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
assert!(Gfx::hd_cell_view(EK::Knight, false, 1, 0).is_none());
|
assert!(Gfx::hd_cell_view(EK::Knight, false, 0, None, false).is_none());
|
||||||
// the 10x10 sprites: every pixel a 3x3 block
|
// the 10x10 sprites: every pixel a 3x3 block; the imp's sack variant
|
||||||
let r = Gfx::sprite_cell_view(EK::Knight, false, 1, 0);
|
let r = Gfx::sprite_cell_view(EK::Knight, false, 1, 0);
|
||||||
for y in 0..SPR_CELL {
|
for y in 0..SPR_CELL {
|
||||||
for x in 0..SPR_CELL {
|
for x in 0..SPR_CELL {
|
||||||
@@ -2475,21 +2620,40 @@ mod sprite_tests {
|
|||||||
assert_eq!((p.r, p.g, p.b, p.a), (q.r, q.g, q.b, q.a));
|
assert_eq!((p.r, p.g, p.b, p.a), (q.r, q.g, q.b, q.a));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
assert!(Gfx::sprite_cell_sack(EK::Knight, false, 1, 0).is_none());
|
||||||
|
for view in 0..VIEWS {
|
||||||
|
let plain = Gfx::sprite_cell_view(EK::Worker, false, 1, view);
|
||||||
|
let sack = Gfx::sprite_cell_sack(EK::Worker, false, 1, view).unwrap();
|
||||||
|
assert!(differ(&img, sack, plain) > 30, "view {view}: the small sack shows");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// `cargo test --bins dump_imp_cells -- --ignored` writes the imp's cells to /tmp for a look
|
// `cargo test --bins dump_imp_cells -- --ignored` writes the imp's cells to /tmp for a look:
|
||||||
|
// rows: 10x10, 10x10 with sack, then idle and the hack frames without and with sack
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn dump_imp_cells() {
|
fn dump_imp_cells() {
|
||||||
let img = gen_sprites();
|
let img = gen_sprites();
|
||||||
let mut out = Image::gen_image_color((ACELL * VIEWS * 4) as u16, (ACELL * 8) as u16, Color::new(0.35, 0.33, 0.38, 1.0));
|
let mut cells: Vec<Vec<Rect>> = Vec::new();
|
||||||
for view in 0..VIEWS {
|
for sack in [false, true] {
|
||||||
for (row, r) in [Gfx::sprite_cell_view(EK::Worker, false, 1, view), Gfx::hd_cell_view(EK::Worker, false, 1, view).unwrap()].iter().enumerate() {
|
let r: Vec<Rect> = (0..VIEWS).map(|v| if sack { Gfx::sprite_cell_sack(EK::Worker, false, 1, v).unwrap() } else { Gfx::sprite_cell_view(EK::Worker, false, 1, v) }).collect();
|
||||||
for y in 0..ACELL * 4 {
|
cells.push(r);
|
||||||
for x in 0..ACELL * 4 {
|
}
|
||||||
let p = img.get_pixel(r.x as u32 + x / 4, r.y as u32 + y / 4);
|
for sack in [false, true] {
|
||||||
|
for hack in [None, Some(0), Some(1), Some(2), Some(3)] {
|
||||||
|
cells.push((0..VIEWS).map(|v| Gfx::hd_cell_view(EK::Worker, false, v, hack, sack).unwrap()).collect());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let z = 3;
|
||||||
|
let (w, h) = (ACELL * z * VIEWS, ACELL * z * cells.len() as u32);
|
||||||
|
let mut out = Image::gen_image_color(w as u16, h as u16, Color::new(0.35, 0.33, 0.38, 1.0));
|
||||||
|
for (row, list) in cells.iter().enumerate() {
|
||||||
|
for (view, r) in list.iter().enumerate() {
|
||||||
|
for y in 0..ACELL * z {
|
||||||
|
for x in 0..ACELL * z {
|
||||||
|
let p = img.get_pixel(r.x as u32 + x / z, r.y as u32 + y / z);
|
||||||
if p.a > 0.5 {
|
if p.a > 0.5 {
|
||||||
out.set_pixel(view * ACELL * 4 + x, (ACELL * 8 - 1) - (row as u32 * ACELL * 4 + y), p);
|
out.set_pixel(view as u32 * ACELL * z + x, h - 1 - (row as u32 * ACELL * z + y), p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1762,7 +1762,18 @@ impl R3d {
|
|||||||
let white = e.flash > 0.07;
|
let white = e.flash > 0.07;
|
||||||
let v = if e.captive != 0 { 0 } else { view };
|
let v = if e.captive != 0 { 0 } else { view };
|
||||||
let detailed = hd && w * 1.2 * px_per_tile >= crate::gfx::ACELL as f32 * 0.85;
|
let detailed = hd && w * 1.2 * px_per_tile >= crate::gfx::ACELL as f32 * 0.85;
|
||||||
let src = detailed.then(|| Gfx::hd_cell_view(e.kind, white, e.level, v)).flatten().unwrap_or_else(|| Gfx::sprite_cell_view(e.kind, white, e.level, v));
|
// treasure in a sack on the back; digging as a drawn animation (detailed sprites),
|
||||||
|
// frame by frame with the same rhythm as the swing below
|
||||||
|
let sack = e.carry > 0 && Gfx::has_sack(e.kind);
|
||||||
|
let hacking = e.working && !e.moving && !sleeping && matches!(e.job, Job::Dig(_) | Job::TearBridge(_));
|
||||||
|
let frame = ((ph * 1.3 / std::f32::consts::TAU).rem_euclid(1.0) * crate::gfx::HACK_FRAMES as f32) as u32;
|
||||||
|
let hd = if detailed { Gfx::hd_cell_view(e.kind, white, v, hacking.then_some(frame), sack) } else { None };
|
||||||
|
if hd.is_some() && hacking {
|
||||||
|
// the frames swing the pickaxe: no tilting of the whole figure
|
||||||
|
rot = 0.0;
|
||||||
|
lunge = 0.0;
|
||||||
|
}
|
||||||
|
let src = hd.or_else(|| sack.then(|| Gfx::sprite_cell_sack(e.kind, white, e.level, v)).flatten()).unwrap_or_else(|| Gfx::sprite_cell_view(e.kind, white, e.level, v));
|
||||||
let (mut u0, v0) = (src.x / sw, src.y / sh);
|
let (mut u0, v0) = (src.x / sw, src.y / sh);
|
||||||
let (mut u1, v1) = ((src.x + src.w) / sw, (src.y + src.h) / sh);
|
let (mut u1, v1) = ((src.x + src.w) / sw, (src.y + src.h) / sh);
|
||||||
if mirror {
|
if mirror {
|
||||||
@@ -1872,7 +1883,8 @@ impl R3d {
|
|||||||
let rise = (k * 14.0 * F() as f32).floor();
|
let rise = (k * 14.0 * F() as f32).floor();
|
||||||
gfx.glyph('z', head.x + cw * 2.0, head.y - GH * S() - rise, S(), Color::new(0.8, 0.85, 1.0, 1.0 - k));
|
gfx.glyph('z', head.x + cw * 2.0, head.y - GH * S() - rise, S(), Color::new(0.8, 0.85, 1.0, 1.0 - k));
|
||||||
}
|
}
|
||||||
if e.carry > 0 {
|
// treasure without a sack sprite: a coin over the unit (imps carry a sack instead)
|
||||||
|
if e.carry > 0 && !Gfx::has_sack(e.kind) {
|
||||||
let p = proj(vec3(e.disp_x, 0.35, e.disp_y));
|
let p = proj(vec3(e.disp_x, 0.35, e.disp_y));
|
||||||
gfx.glyph('☼', p.x + cw * 2.0, p.y - GH * small, small, Color::from_rgba(250, 210, 60, 255));
|
gfx.glyph('☼', p.x + cw * 2.0, p.y - GH * small, small, Color::from_rgba(250, 210, 60, 255));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,27 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Turn a generated sprite sheet (one figure in several views on a magenta background) into
|
"""Turn a generated sprite sheet (figures in a grid on a magenta background) into clean
|
||||||
clean pixel-art sprites: background removed, every view cut out, sampled down to a fixed pixel
|
pixel-art sprites: background removed, every figure cut out, sampled down to a fixed pixel
|
||||||
grid and reduced to one shared palette.
|
grid and reduced to a palette.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
pixelize_sprite.py raw.png out_prefix --views 5 --size 24 [--crop x0,y0,x1,y1] [--colors 16]
|
pixelize_sprite.py raw.png out_prefix [--grid ROWSxCOLS | --views N] --size 32
|
||||||
|
[--crop x0,y0,x1,y1] [--colors 16] [--step S] [--feet Y]
|
||||||
|
[--palette sheet.png] [--add-colors R,G,B;R,G,B] [--measure]
|
||||||
|
|
||||||
--crop takes fractions of the image (0..1) to use only a part of it (e.g. one row of a sheet).
|
--grid / --views layout of the figures in the raw sheet (--views N = one row of N)
|
||||||
Writes out_prefix-<size>.png (the views side by side, size x size each, transparent) and
|
--crop fractions of the image (0..1) to use only a part of it
|
||||||
out_prefix-<size>-preview.png (scaled up 8x on a dark background for viewing).
|
--size output cell (size x size pixels)
|
||||||
Needs Pillow.
|
--step source pixels per output pixel; default: the tallest or widest figure fills
|
||||||
|
the cell. Give the same body scale to several sheets of one character with
|
||||||
|
--measure (prints every figure's box in source pixels) and an explicit step.
|
||||||
|
--feet the output row the feet stand on (default: the bottom row)
|
||||||
|
--palette take the colours of an existing sprite sheet instead of choosing new ones
|
||||||
|
(frames of one character keep exactly the same colours)
|
||||||
|
--add-colors extra palette colours (e.g. the light iron of a tool the reference lacks)
|
||||||
|
--outline-weight weight of near-black pixels when sampling (below 1: thin details survive)
|
||||||
|
|
||||||
|
Writes out_prefix-<size>.png (cells in the grid's order, row by row; transparent) and
|
||||||
|
out_prefix-<size>-preview.png (scaled up 8x on a grey background for viewing). Needs Pillow.
|
||||||
"""
|
"""
|
||||||
import argparse
|
import argparse
|
||||||
import math
|
import math
|
||||||
@@ -26,16 +38,18 @@ def is_background(p):
|
|||||||
return magenta or frame
|
return magenta or frame
|
||||||
|
|
||||||
|
|
||||||
def figure_boxes(img, views):
|
def figure_boxes(img, rows, cols):
|
||||||
w, h = img.size
|
w, h = img.size
|
||||||
px = img.load()
|
px = img.load()
|
||||||
mask = [[not is_background(px[x, y]) for x in range(w)] for y in range(h)]
|
mask = [[not is_background(px[x, y]) for x in range(w)] for y in range(h)]
|
||||||
boxes = []
|
boxes = []
|
||||||
for v in range(views):
|
for r in range(rows):
|
||||||
x0, x1 = v * w // views, (v + 1) * w // views
|
y0, y1 = r * h // rows, (r + 1) * h // rows
|
||||||
cols = [x for x in range(x0, x1) if sum(mask[y][x] for y in range(h)) > 3]
|
for c in range(cols):
|
||||||
rows = [y for y in range(h) if sum(mask[y][x] for x in range(x0, x1)) > 3]
|
x0, x1 = c * w // cols, (c + 1) * w // cols
|
||||||
boxes.append((min(cols), min(rows), max(cols) + 1, max(rows) + 1))
|
xs = [x for x in range(x0, x1) if sum(mask[y][x] for y in range(y0, y1)) > 3]
|
||||||
|
ys = [y for y in range(y0, y1) if sum(mask[y][x] for x in range(x0, x1)) > 3]
|
||||||
|
boxes.append((min(xs), min(ys), max(xs) + 1, max(ys) + 1))
|
||||||
return mask, boxes
|
return mask, boxes
|
||||||
|
|
||||||
|
|
||||||
@@ -44,31 +58,44 @@ def main():
|
|||||||
ap.add_argument("raw")
|
ap.add_argument("raw")
|
||||||
ap.add_argument("out")
|
ap.add_argument("out")
|
||||||
ap.add_argument("--views", type=int, default=5)
|
ap.add_argument("--views", type=int, default=5)
|
||||||
|
ap.add_argument("--grid", default=None)
|
||||||
ap.add_argument("--size", type=int, default=24)
|
ap.add_argument("--size", type=int, default=24)
|
||||||
ap.add_argument("--colors", type=int, default=16)
|
ap.add_argument("--colors", type=int, default=16)
|
||||||
ap.add_argument("--crop", default=None)
|
ap.add_argument("--crop", default=None)
|
||||||
|
ap.add_argument("--step", type=float, default=None)
|
||||||
|
ap.add_argument("--feet", type=int, default=None)
|
||||||
|
ap.add_argument("--palette", default=None)
|
||||||
|
ap.add_argument("--add-colors", default="")
|
||||||
|
ap.add_argument("--outline-weight", type=float, default=1.0)
|
||||||
|
ap.add_argument("--measure", action="store_true")
|
||||||
a = ap.parse_args()
|
a = ap.parse_args()
|
||||||
|
rows, cols = (int(v) for v in a.grid.split("x")) if a.grid else (1, a.views)
|
||||||
|
feet = a.size - 1 if a.feet is None else a.feet
|
||||||
|
|
||||||
img = Image.open(a.raw).convert("RGB")
|
img = Image.open(a.raw).convert("RGB")
|
||||||
if a.crop:
|
if a.crop:
|
||||||
f = [float(v) for v in a.crop.split(",")]
|
f = [float(v) for v in a.crop.split(",")]
|
||||||
w, h = img.size
|
w, h = img.size
|
||||||
img = img.crop((int(f[0] * w), int(f[1] * h), int(f[2] * w), int(f[3] * h)))
|
img = img.crop((int(f[0] * w), int(f[1] * h), int(f[2] * w), int(f[3] * h)))
|
||||||
mask, boxes = figure_boxes(img, a.views)
|
mask, boxes = figure_boxes(img, rows, cols)
|
||||||
|
if a.measure:
|
||||||
|
for i, (x0, y0, x1, y1) in enumerate(boxes):
|
||||||
|
print(f"figure {i} (row {i // cols}, column {i % cols}): {x1 - x0} x {y1 - y0} source pixels")
|
||||||
|
return
|
||||||
px = img.load()
|
px = img.load()
|
||||||
# one scale for all views: the tallest or widest figure fills the cell
|
# default: one scale for all figures, the tallest or widest fills the cell
|
||||||
step = max(max(b[3] - b[1], b[2] - b[0]) for b in boxes) / a.size
|
step = a.step or max(max(b[3] - b[1], b[2] - b[0]) for b in boxes) / a.size
|
||||||
|
|
||||||
grids = []
|
grids = []
|
||||||
for (x0, y0, x1, y1) in boxes:
|
for (x0, y0, x1, y1) in boxes:
|
||||||
gw, gh = round((x1 - x0) / step), round((y1 - y0) / step)
|
gw, gh = round((x1 - x0) / step), round((y1 - y0) / step)
|
||||||
grid = [[None] * a.size for _ in range(a.size)]
|
grid = [[None] * a.size for _ in range(a.size)]
|
||||||
ox, oy = (a.size - gw) // 2, a.size - gh # centred, standing on the bottom
|
ox, oy = (a.size - gw) // 2, feet + 1 - gh # centred, the feet on the feet row
|
||||||
for gy in range(gh):
|
for gy in range(gh):
|
||||||
for gx in range(gw):
|
for gx in range(gw):
|
||||||
# the commonest colour of the block's inner part (edges blur between pixels)
|
# the commonest colour of the block's inner part (edges blur between pixels)
|
||||||
cx0, cy0 = x0 + gx * step, y0 + gy * step
|
cx0, cy0 = x0 + gx * step, y0 + gy * step
|
||||||
votes, bg = Counter(), 0
|
votes, bg, fg = Counter(), 0, 0
|
||||||
for sy in range(4):
|
for sy in range(4):
|
||||||
for sx in range(4):
|
for sx in range(4):
|
||||||
x = int(cx0 + (sx + 0.5) * step / 4)
|
x = int(cx0 + (sx + 0.5) * step / 4)
|
||||||
@@ -77,34 +104,43 @@ def main():
|
|||||||
bg += 1
|
bg += 1
|
||||||
else:
|
else:
|
||||||
r, g, b = px[x, y]
|
r, g, b = px[x, y]
|
||||||
votes[(r // 8 * 8, g // 8 * 8, b // 8 * 8)] += 1
|
fg += 1
|
||||||
if votes and sum(votes.values()) >= bg and 0 <= gy + oy < a.size and 0 <= gx + ox < a.size:
|
# the outline counts a little less, so thin coloured parts
|
||||||
|
# (a tool's iron, a handle) are not swallowed by their outline
|
||||||
|
votes[(r // 8 * 8, g // 8 * 8, b // 8 * 8)] += a.outline_weight if r + g + b < 90 else 1
|
||||||
|
if votes and fg >= bg and 0 <= gy + oy < a.size and 0 <= gx + ox < a.size:
|
||||||
grid[gy + oy][gx + ox] = votes.most_common(1)[0][0]
|
grid[gy + oy][gx + ox] = votes.most_common(1)[0][0]
|
||||||
grids.append(grid)
|
grids.append(grid)
|
||||||
|
|
||||||
# one shared palette for all views: farthest-point picking weighted by how often a colour
|
|
||||||
# occurs, so small but distinct details (eyes, the tool's iron) keep their own colour
|
|
||||||
# instead of melting into the large areas
|
|
||||||
used = [c for g in grids for row in g for c in row if c]
|
used = [c for g in grids for row in g for c in row if c]
|
||||||
counts = Counter(used)
|
counts = Counter(used)
|
||||||
cands = [c for c, n in counts.items() if n >= 3] or list(counts)
|
|
||||||
dist = lambda p, q: sum((x - y) ** 2 for x, y in zip(p, q)) ** 0.5
|
dist = lambda p, q: sum((x - y) ** 2 for x, y in zip(p, q)) ** 0.5
|
||||||
palette = [counts.most_common(1)[0][0]]
|
if a.palette:
|
||||||
while len(palette) < min(a.colors, len(cands)):
|
ref = Image.open(a.palette).convert("RGBA")
|
||||||
palette.append(max(cands, key=lambda c: min(dist(c, q) for q in palette) * math.log(1 + counts[c])))
|
palette = sorted({p[:3] for p in ref.get_flattened_data() if p[3] > 128})
|
||||||
|
else:
|
||||||
|
# farthest-point picking weighted by how often a colour occurs, so small but distinct
|
||||||
|
# details (eyes, the tool's iron) keep their own colour instead of melting into the
|
||||||
|
# large areas
|
||||||
|
cands = [c for c, n in counts.items() if n >= 3] or list(counts)
|
||||||
|
palette = [counts.most_common(1)[0][0]]
|
||||||
|
while len(palette) < min(a.colors, len(cands)):
|
||||||
|
palette.append(max(cands, key=lambda c: min(dist(c, q) for q in palette) * math.log(1 + counts[c])))
|
||||||
|
palette += [tuple(int(v) for v in c.split(",")) for c in a.add_colors.split(";") if c]
|
||||||
near = {c: min(palette, key=lambda q: dist(c, q)) for c in counts}
|
near = {c: min(palette, key=lambda q: dist(c, q)) for c in counts}
|
||||||
|
|
||||||
sheet = Image.new("RGBA", (a.size * a.views, a.size), (0, 0, 0, 0))
|
sheet = Image.new("RGBA", (a.size * cols, a.size * rows), (0, 0, 0, 0))
|
||||||
for v, g in enumerate(grids):
|
for i, g in enumerate(grids):
|
||||||
|
cx, cy = (i % cols) * a.size, (i // cols) * a.size
|
||||||
for y, row in enumerate(g):
|
for y, row in enumerate(g):
|
||||||
for x, c in enumerate(row):
|
for x, c in enumerate(row):
|
||||||
if c:
|
if c:
|
||||||
sheet.putpixel((v * a.size + x, y), near[c] + (255,))
|
sheet.putpixel((cx + x, cy + y), near[c] + (255,))
|
||||||
sheet.save(f"{a.out}-{a.size}.png")
|
sheet.save(f"{a.out}-{a.size}.png")
|
||||||
prev = Image.new("RGBA", sheet.size, (96, 92, 104, 255))
|
prev = Image.new("RGBA", sheet.size, (96, 92, 104, 255))
|
||||||
prev.alpha_composite(sheet)
|
prev.alpha_composite(sheet)
|
||||||
prev.resize((sheet.width * 8, sheet.height * 8), Image.NEAREST).save(f"{a.out}-{a.size}-preview.png")
|
prev.resize((sheet.width * 8, sheet.height * 8), Image.NEAREST).save(f"{a.out}-{a.size}-preview.png")
|
||||||
print(f"{a.out}-{a.size}.png: {a.views} views, {a.size}px, {len(set(near.values()))} colours")
|
print(f"{a.out}-{a.size}.png: {rows}x{cols} figures, {a.size}px, step {step:.1f}, {len(set(near.values()))} colours")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||