From fdb28e93af2ee6413659ab4a773f80a9b67a3100 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Mon, 28 Sep 2026 15:58:36 +0800 Subject: art(p39): 自绘中式元素补入 cc0 图集(索引 12-18,608x32) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/make-cc0-art.py 由 384x32(12格) 扩到 608x32(19格),新增 draw_bridge_center/end、draw_boat、draw_stone_lantern、draw_stall、draw_reeds - 旧索引 0-11 逐字节不变;产物 1523 bytes,sha256 ef86ced2…6adcb,二次生成一致 - docs/素材来源.md 更新为 19 格并新增第 4 节(P39 清单 + 摆放规则 + fallback 差异) --- scripts/make-cc0-art.py | 201 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 197 insertions(+), 4 deletions(-) (limited to 'scripts') diff --git a/scripts/make-cc0-art.py b/scripts/make-cc0-art.py index 44bd013..7c5868c 100644 --- a/scripts/make-cc0-art.py +++ b/scripts/make-cc0-art.py @@ -4,7 +4,7 @@ Reads the provenanced CC0 packs in third_party/ (Kenney Tiny Farm tiles, Puny Characters) and emits deterministic atlases for the prototype: - src/LivingVillage.Desktop/Assets/cc0-tiles.png 384x32 (12 tiles, 32px each) + src/LivingVillage.Desktop/Assets/cc0-tiles.png 608x32 (19 tiles, 32px each) src/LivingVillage.Desktop/Assets/cc0-characters.png 3840x48 (5 variants x 4 directions x 6 frames, 32x48 cells) Ground tiles (indices 0-3) come from Kenney Tiny Farm via integer 2x NEAREST of @@ -13,6 +13,14 @@ art (it is a farm pack), so indices 4-11 are *self-drawn* 32px pixel art in a consistent Jiangnan palette (white plaster, dark slate tile, warm wood). Each tile's provenance is recorded in docs/素材来源.md. +P39 (indices 12-18) appends more self-drawn Jiangnan elements after the P34 +block, keeping every older index byte-for-byte backwards compatible: + 12 bridge centre / 13 bridge end / 14 boat left / 15 boat right + 16 stone lantern / 17 market stall / 18 reeds +Bridge/boat/lantern/stall/reeds are all self-drawn (Kenney Tiny Farm has none +of them), same palette, same 32px grid, NEAREST-safe (no resampling: drawn at +final 1x resolution, so pixel edges are exact). + Mapping strategy (16 -> 32): integer 2x nearest-neighbour upscale of every source sprite; ground tiles fill the 32x32 cell exactly; Puny sprites keep their 16x24-ish content band (2x -> 32x48) anchored to the cell bottom. @@ -197,8 +205,181 @@ def draw_door(c, ox): c.fill(ox + 20, 17, 3, 3, (222, 190, 120, 255)) +# --- P39 self-drawn Jiangnan elements (indices 12-18) ------------------------- +STONE = (150, 150, 142, 255) +STONE_DARK = (104, 106, 102, 255) +STONE_HI = (196, 196, 188, 255) +BRIDGE_DECK = (128, 128, 122, 255) +BOAT_HULL = (58, 48, 40, 255) +BOAT_HULL_DARK = (38, 32, 27, 255) +BOAT_ROOF = (46, 54, 42, 255) +BOAT_ROOF_HI = (76, 88, 66, 255) +LANTERN_PAPER = (228, 196, 128, 255) +LANTERN_GLOW = (255, 208, 150, 255) +LANTERN_GLOW_HI = (255, 232, 190, 255) +LANTERN_STONE = (168, 166, 156, 255) +REED_STEM = (74, 104, 60, 255) +REED_LEAF = (92, 130, 72, 255) +REED_TIP = (146, 158, 96, 255) +STALL_AWNING = (176, 74, 58, 255) +STALL_AWNING_2 = (196, 176, 132, 255) +CARROT = (216, 126, 54, 255) +CABBAGE = (120, 168, 84, 255) +CLOTH = (208, 198, 172, 255) + + +def draw_bridge_center(c, ox): + """Arch bridge span: stone deck with a railing and an arched underside. + + The lower third is transparent so the water ripple still shows through the + arch; a warm railing sits on top of the deck. + """ + c.fill(ox, 0, 32, 32, CLEAR) + # Deck (single stone span, bright top edge / dark underside). + c.fill(ox, 10, 32, 9, BRIDGE_DECK) + c.fill(ox, 10, 32, 2, STONE_HI) + c.fill(ox, 17, 32, 2, STONE_DARK) + # Arch cut-outs under the deck (transparent so water shows through). + c.fill(ox + 6, 19, 8, 13, CLEAR) + c.fill(ox + 18, 19, 8, 13, CLEAR) + c.fill(ox + 10, 19, 12, 5, BRIDGE_DECK) + # Wood railing above the deck. + c.fill(ox, 4, 32, 2, WOOD_DARK) + c.fill(ox, 7, 32, 2, WOOD) + for px in (3, 14, 25): + c.fill(ox + px, 4, 2, 6, WOOD) + c.set(ox + px, 9, WOOD_DARK) + + +def draw_bridge_end(c, ox): + """Bridge end: stone abutment at the bank plus the last railing posts.""" + c.fill(ox, 0, 32, 32, CLEAR) + # Stepped abutment: two stone steps widening down to the bank. + c.fill(ox + 4, 12, 24, 6, STONE) + c.fill(ox + 4, 12, 24, 2, STONE_HI) + c.fill(ox, 18, 32, 14, STONE) + c.fill(ox, 18, 32, 2, STONE_HI) + c.fill(ox, 29, 32, 3, STONE_DARK) + # End posts of the railing. + c.fill(ox + 2, 3, 3, 11, WOOD_DARK) + c.fill(ox + 2, 3, 3, 2, WOOD) + c.fill(ox + 27, 3, 3, 11, WOOD_DARK) + c.fill(ox + 27, 3, 3, 2, WOOD) + c.fill(ox + 2, 6, 28, 2, WOOD) + + +def draw_boat(c, ox, right_half): + """Wu Peng boat half (2 tiles wide): dark hull + curved bamboo awning. + + Drawn as two halves so a 2-tile boat reads as one continuous silhouette. + The hull floats on the water: its top edge (gunwale) is 4px down, so open + water ripple stays visible above the boat on the tile. + """ + c.fill(ox, 0, 32, 32, CLEAR) + if not right_half: + # Left half: bow rising to the left, hull tapering. + for i in range(26): + half = 3 + i // 3 + c.fill(ox + 0, 10 + i, half, 1, BOAT_HULL) + c.fill(ox, 10, 3, 1, BOAT_HULL_DARK) + c.fill(ox + 12, 9, 20, 2, BOAT_HULL_DARK) + c.fill(ox + 12, 11, 20, 13, BOAT_HULL) + # Awning (wu peng) left edge, arch top. + c.fill(ox + 12, 1, 20, 8, BOAT_ROOF) + c.fill(ox + 12, 1, 20, 2, BOAT_ROOF_HI) + c.fill(ox + 12, 1, 2, 8, BOAT_ROOF_HI) + c.fill(ox + 12, 8, 20, 2, BOAT_HULL_DARK) + # Stern post at the extreme left of the visible half. + c.fill(ox, 6, 2, 6, BOAT_HULL_DARK) + else: + # Right half: stern rising to the right, mirrored continuous hull. + for i in range(26): + half = 3 + i // 3 + c.fill(ox + 32 - half, 10 + i, half, 1, BOAT_HULL) + c.fill(ox + 29, 10, 3, 1, BOAT_HULL_DARK) + c.fill(ox, 9, 20, 2, BOAT_HULL_DARK) + c.fill(ox, 11, 20, 13, BOAT_HULL) + c.fill(ox, 1, 20, 8, BOAT_ROOF) + c.fill(ox, 1, 20, 2, BOAT_ROOF_HI) + c.fill(ox + 18, 1, 2, 8, BOAT_ROOF_HI) + c.fill(ox, 8, 20, 2, BOAT_HULL_DARK) + c.fill(ox + 30, 6, 2, 6, BOAT_HULL_DARK) + + +def draw_stone_lantern(c, ox): + """Ishidoro-style stone lantern with a warm lit window (night glow anchor). + + The lit pane is a bright warm core (alpha opaque) sitting inside a stone + housing, so the renderer's radial glow layer adds a real halo on top. + """ + c.fill(ox, 0, 32, 32, CLEAR) + # Base plinth. + c.fill(ox + 8, 27, 16, 5, LANTERN_STONE) + c.fill(ox + 8, 27, 16, 1, STONE_HI) + c.fill(ox + 11, 24, 10, 4, LANTERN_STONE) + # Shaft. + c.fill(ox + 14, 18, 4, 7, LANTERN_STONE) + # Light housing with a bright warm pane. + c.fill(ox + 6, 9, 20, 10, LANTERN_STONE) + c.fill(ox + 6, 9, 20, 1, STONE_HI) + c.fill(ox + 9, 11, 14, 7, LANTERN_GLOW) + c.fill(ox + 11, 12, 4, 5, LANTERN_GLOW_HI) + # Dark frame over the pane. + c.vline(ox + 9, 11, 7, STONE_DARK) + c.vline(ox + 22, 11, 7, STONE_DARK) + c.vline(ox + 15, 11, 7, STONE_DARK) + # Cap + finial. + c.fill(ox + 4, 6, 24, 4, STONE_DARK) + c.fill(ox + 6, 5, 20, 2, LANTERN_STONE) + c.fill(ox + 15, 1, 3, 4, LANTERN_STONE) + + +def draw_stall(c, ox): + """Roadside market stall: striped awning + counter + produce baskets.""" + c.fill(ox, 0, 32, 32, CLEAR) + # Awning: red/cream stripes with a scalloped lower edge. + for x in range(0, 32, 4): + stripe = STALL_AWNING if (x // 4) % 2 == 0 else STALL_AWNING_2 + c.fill(ox + x, 6, 4, 8, stripe) + c.fill(ox, 13, 32, 2, WOOD_DARK) + for x in range(0, 32, 8): + c.fill(ox + x, 15, 8, 2, STALL_AWNING_2) + c.set(ox + x + 4, 17, WOOD_DARK) + # Support poles. + c.vline(ox + 2, 15, 13, WOOD_DARK) + c.vline(ox + 28, 15, 13, WOOD_DARK) + # Counter. + c.fill(ox + 2, 20, 28, 4, WOOD) + c.fill(ox + 2, 20, 28, 1, WOOD_DARK) + # Produce on the counter. + c.fill(ox + 6, 17, 4, 3, CARROT) + c.fill(ox + 7, 16, 2, 1, REED_LEAF) + c.fill(ox + 14, 16, 6, 4, CABBAGE) + c.fill(ox + 15, 15, 4, 1, REED_LEAF) + c.fill(ox + 24, 17, 4, 3, CLOTH) + # Base. + c.fill(ox + 2, 24, 28, 4, WOOD_DARK) + + +def draw_reeds(c, ox): + """Waterside reeds: a few stems with leaf blades and seed tips.""" + c.fill(ox, 0, 32, 32, CLEAR) + stems = [(6, 6, 24), (13, 10, 22), (21, 7, 23), (26, 13, 20)] + for (x, top, bottom) in stems: + c.fill(ox + x, top, 2, bottom - top, REED_STEM) + c.fill(ox + x, top, 2, 3, REED_TIP) + # Leaf blades angling off the stem. + c.fill(ox + x - 4, top + 5, 5, 1, REED_LEAF) + c.fill(ox + x - 3, top + 6, 4, 1, REED_LEAF) + c.fill(ox + x + 2, top + 9, 4, 1, REED_LEAF) + # Dense root cluster at the waterline (bottom of the tile). + c.fill(ox + 3, 28, 26, 4, REED_LEAF) + c.fill(ox + 8, 30, 16, 2, REED_STEM) + + + def build_cc0_tile_atlas(): - """Twelve 32x32 tiles: 4 Kenney ground + 8 self-drawn structure tiles. + """Nineteen 32x32 tiles: 4 Kenney ground + 15 self-drawn Jiangnan tiles. Ground source is the *packed* sheet (tilemap_packed.png) which has NO spacing between tiles: 192x176 = 12x11 tiles at a 16px pitch (the spaced tilemap.png @@ -208,8 +389,12 @@ def build_cc0_tile_atlas(): wall, railing or door tiles), layout fixed by the P34 atlas contract: 4 water A / 5 water B / 6 roof ridge / 7 roof eave 8 wall wood / 9 wall window / 10 railing / 11 door + P39 appends indices 12-18 (bridge / boat / lantern / stall / reeds) after the + P34 block so every older index stays unchanged and backwards compatible: + 12 bridge centre / 13 bridge end / 14 boat left / 15 boat right + 16 stone lantern / 17 market stall / 18 reeds """ - c = Canvas(384, 32) + c = Canvas(608, 32) packed = Image.open(THIRD / "kenney_tiny-farm" / "Tilemap" / "tilemap_packed.png").convert("RGBA") def tile(grid_x, grid_y): @@ -227,6 +412,14 @@ def build_cc0_tile_atlas(): draw_wall(c, 9 * 32, window=True) draw_railing(c, 10 * 32) draw_door(c, 11 * 32) + # P39 appended block. + draw_bridge_center(c, 12 * 32) + draw_bridge_end(c, 13 * 32) + draw_boat(c, 14 * 32, right_half=False) + draw_boat(c, 15 * 32, right_half=True) + draw_stone_lantern(c, 16 * 32) + draw_stall(c, 17 * 32) + draw_reeds(c, 18 * 32) return c @@ -281,7 +474,7 @@ def build_cc0_character_atlas(): def main(): ASSETS.mkdir(parents=True, exist_ok=True) targets = [ - ("cc0-tiles.png", build_cc0_tile_atlas(), 384, 32), + ("cc0-tiles.png", build_cc0_tile_atlas(), 608, 32), ("cc0-characters.png", build_cc0_character_atlas(), 3840, 48), ] for name, canvas, w, h in targets: -- cgit v1.2.3