feat: htmx derivation home page 1:1 from react
This commit is contained in:
100
htmx-demo/compare.py
Normal file
100
htmx-demo/compare.py
Normal file
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Image comparator for React vs HTMX showcase validation.
|
||||
|
||||
Compares two PNG/WEBP screenshots and produces:
|
||||
1. Similarity percentage (pixels within tolerance / total pixels)
|
||||
2. A diff image with mismatches highlighted magenta on a faded background
|
||||
|
||||
Tolerance is per-channel: anti-aliasing / sub-pixel hinting is accepted
|
||||
(default 12 of 255 per channel, tweakable via --tol). Font / layout / color
|
||||
changes produce large regions of divergence that will exceed the tolerance.
|
||||
|
||||
Usage:
|
||||
python3 compare.py react.webp htmx.webp [--out diff.webp] [--tol 12]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageChops, ImageDraw
|
||||
|
||||
|
||||
def load(path):
|
||||
img = Image.open(path).convert("RGB")
|
||||
return img
|
||||
|
||||
|
||||
def compare(react_path, htmx_path, out_path, tol):
|
||||
a = load(react_path)
|
||||
b = load(htmx_path)
|
||||
|
||||
if a.size != b.size:
|
||||
# Pad the smaller one with transparent/white so we can still diff
|
||||
w, h = max(a.width, b.width), max(a.height, b.height)
|
||||
pad_a = Image.new("RGB", (w, h), (255, 255, 255))
|
||||
pad_b = Image.new("RGB", (w, h), (255, 255, 255))
|
||||
pad_a.paste(a, (0, 0))
|
||||
pad_b.paste(b, (0, 0))
|
||||
a, b = pad_a, pad_b
|
||||
size_mismatch = True
|
||||
else:
|
||||
size_mismatch = False
|
||||
|
||||
diff = ImageChops.difference(a, b)
|
||||
# Per-pixel max channel diff
|
||||
total = a.width * a.height
|
||||
differing = 0
|
||||
mask = Image.new("L", a.size, 0)
|
||||
mask_pixels = mask.load()
|
||||
diff_pixels = diff.load()
|
||||
for y in range(a.height):
|
||||
for x in range(a.width):
|
||||
r, g, bl = diff_pixels[x, y]
|
||||
if max(r, g, bl) > tol:
|
||||
differing += 1
|
||||
mask_pixels[x, y] = 255
|
||||
|
||||
similarity = 100.0 * (total - differing) / total
|
||||
|
||||
# Build diff image: React screenshot faded 50%, with diffs in magenta
|
||||
faded = Image.eval(a, lambda v: int(v * 0.4 + 0.6 * 255))
|
||||
magenta = Image.new("RGB", a.size, (255, 0, 180))
|
||||
out = Image.composite(magenta, faded, mask)
|
||||
|
||||
# Add a header text
|
||||
draw = ImageDraw.Draw(out)
|
||||
header = (
|
||||
f"similarity={similarity:.2f}% "
|
||||
f"differing={differing}/{total} "
|
||||
f"tol={tol}"
|
||||
+ (" (SIZE MISMATCH — padded)" if size_mismatch else "")
|
||||
)
|
||||
draw.rectangle([0, 0, a.width, 24], fill=(0, 0, 0))
|
||||
draw.text((8, 4), header, fill=(255, 255, 255))
|
||||
|
||||
out.save(out_path)
|
||||
return similarity, differing, total
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("react")
|
||||
p.add_argument("htmx")
|
||||
p.add_argument("--out", default="diff.webp")
|
||||
p.add_argument("--tol", type=int, default=12)
|
||||
args = p.parse_args()
|
||||
|
||||
sim, diff_px, total = compare(args.react, args.htmx, args.out, args.tol)
|
||||
print(f"react = {args.react}")
|
||||
print(f"htmx = {args.htmx}")
|
||||
print(f"diff -> {args.out}")
|
||||
print(f" similarity = {sim:.2f}% ({diff_px} / {total} pixels differ > {args.tol})")
|
||||
if sim < 99.5:
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user