Files
benilla-octo/scripts/make-map-feather.py
T
gromandClaude Opus 5 758665d4d8 map: a 1.12 zone sheet is opaque, so the feather belongs on
What was left after the stitch's geometry was hard rectangles at the
zone zooms. Bisected with OpenMapClassic's own /omc layer switches:
baked and gaps draw nothing on this build, zoneart draws them all.

A zone's sheet is masked to its land only for ids in ZoneMaskIDs, the
contiguous Classic range 1411-1452. On this realm that leaves
thirty-four without one -- every city, the dungeons, and every zone the
realm added of its own. Cities already decline to draw unmasked; zones
do not, and Gilneas puts its whole sheet, painted sea and torn parchment
border, over Silverpine.

PlaceBase takes a feather for exactly this and the zone walk passed nil
behind `(not era) and FEATHER`. Sound on Era, where a C_Map zone texture
carries a transparent surround; not true here, where the realm's sheets
are alpha 255 across every texel. The port passes it. PlaceBase prefers
the land mask where there is one, so the forty masked zones are
untouched -- 0.57% of pixels moved in a Dun Morogh view.

scripts/make-map-feather.py generates the three masks as 256x256
uncompressed TGA. A feather is a smoothstep ramp, which upstream's own
NOTICE calls generated rather than cut from art. A per-zone LAND mask
was deliberately not attempted: on this art sea and land do not separate
by colour (a sea patch reads R-B +28, a land patch +34) and the realm's
zone bitmap is 13x13 cells for its largest zone. Either would be a guess
dressed as art.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-03 18:17:14 -07:00

63 lines
2.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""Generate OpenMapClassic's edge-feather masks.
A feather is a mask over a map sheet's whole rect: opaque in the middle, falling to nothing at
the border, so the sheet blends into what is around it instead of ending in a straight line.
Upstream's own NOTICE calls these "generated from noise" rather than cut from art -- they are
parametric, so this reproduces them rather than approximating an artist.
1.12 needs them where Classic Era does not. Era's `C_Map` zone textures carry a transparent
surround; the 1.12 sheets in this client's archives are **fully opaque** (measured: alpha 255
everywhere) and paint their own sea, lettering and a torn parchment border right to the edge. So
a zone drawn without a mask covers its neighbours with a rectangle -- which is what
`OpenMapClassic/Map/Stitch.lua`'s own comment predicts, and what the Gilneas sheet does over
Silverpine.
Writes 256x256 uncompressed 32-bit TGA, matching the shipped `MapZoneDilate` masks: RGB black,
alpha the coverage.
"""
import sys
from pathlib import Path
import numpy as np
from PIL import Image
SIZE = 256
# Margin as a fraction of the sheet, per variant. The parchment border on a 1002-px sheet runs
# ~35 px (3.5%); a feather has to clear that and then blend, so the plain one takes 9%.
VARIANTS = {"MapFeather": 0.09, "MapFeatherThin": 0.045, "MapFeatherWide": 0.16}
def ramp(n: int, margin: float) -> np.ndarray:
"""1 in the middle, 0 at both ends, smoothstep across `margin` of the span."""
x = (np.arange(n) + 0.5) / n
d = np.minimum(x, 1.0 - x) / margin # distance from the nearer edge, in margins
t = np.clip(d, 0.0, 1.0)
return t * t * (3.0 - 2.0 * t)
def feather(margin: float) -> Image.Image:
a = np.outer(ramp(SIZE, margin), ramp(SIZE, margin))
rgba = np.zeros((SIZE, SIZE, 4), dtype=np.uint8)
rgba[:, :, 3] = np.round(a * 255.0).astype(np.uint8)
return Image.fromarray(rgba, "RGBA")
def main() -> int:
if len(sys.argv) != 2:
print("usage: make-map-feather.py <OpenMapClassic/Media/Textures>", file=sys.stderr)
return 2
out = Path(sys.argv[1])
if not out.is_dir():
print(f"not a directory: {out}", file=sys.stderr)
return 2
for name, margin in VARIANTS.items():
path = out / f"{name}.tga"
feather(margin).save(path)
print(f"wrote {path} ({margin:.0%} margin)")
return 0
if __name__ == "__main__":
raise SystemExit(main())