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
63 lines
2.4 KiB
Python
Executable File
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())
|