@creator-notes/cnotes 0.35.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env python3
2
+ """Soft radial spotlight post-process for how-to filmstrip frames.
3
+
4
+ Use when the in-page CSS overlay was weak or missing. Centres are CSS pixels
5
+ from getBoundingClientRect; pass --dpr for retina screenshots (often 2).
6
+
7
+ Example:
8
+ python3 soft-spotlight.py --in A3.png --out A3.png --cx 276 --cy 219 --dpr 2 --opacity 0.55
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ from pathlib import Path
15
+
16
+ import numpy as np
17
+ from PIL import Image
18
+
19
+
20
+ def soft_spotlight(
21
+ im: Image.Image,
22
+ cx: float,
23
+ cy: float,
24
+ hole_r: float = 180,
25
+ max_opacity: float = 0.4,
26
+ fade_mult: float = 5.5,
27
+ ) -> Image.Image:
28
+ w, h = im.size
29
+ ys = np.arange(h)[:, None]
30
+ xs = np.arange(w)[None, :]
31
+ dist = np.sqrt((xs - cx) ** 2 + (ys - cy) ** 2)
32
+ ramp_end = hole_r * fade_mult
33
+ t = np.clip((dist - hole_r) / max(ramp_end - hole_r, 1), 0, 1)
34
+ t = t * t * (3 - 2 * t) # smoothstep
35
+ alpha = (t * max_opacity * 255).astype(np.uint8)
36
+ overlay = np.zeros((h, w, 4), dtype=np.uint8)
37
+ overlay[..., 3] = alpha
38
+ base = im.convert("RGBA")
39
+ return Image.alpha_composite(base, Image.fromarray(overlay, "RGBA"))
40
+
41
+
42
+ def main() -> None:
43
+ p = argparse.ArgumentParser(description=__doc__)
44
+ p.add_argument("--in", dest="src", required=True, help="Input PNG")
45
+ p.add_argument("--out", dest="dst", required=True, help="Output PNG")
46
+ p.add_argument("--cx", type=float, required=True, help="Centre X in CSS pixels")
47
+ p.add_argument("--cy", type=float, required=True, help="Centre Y in CSS pixels")
48
+ p.add_argument("--dpr", type=float, default=1.0, help="Device pixel ratio (default 1)")
49
+ p.add_argument("--hole", type=float, default=None, help="Clear hole radius in device pixels")
50
+ p.add_argument("--opacity", type=float, default=0.4, help="Max dim opacity 0–1")
51
+ p.add_argument("--fade-mult", type=float, default=5.5, help="Fade length as multiple of hole")
52
+ args = p.parse_args()
53
+
54
+ src = Path(args.src)
55
+ dst = Path(args.dst)
56
+ im = Image.open(src)
57
+ cx = args.cx * args.dpr
58
+ cy = args.cy * args.dpr
59
+ hole = args.hole if args.hole is not None else max(160.0, 90.0 * args.dpr)
60
+
61
+ out = soft_spotlight(
62
+ im,
63
+ cx,
64
+ cy,
65
+ hole_r=hole,
66
+ max_opacity=args.opacity,
67
+ fade_mult=args.fade_mult,
68
+ )
69
+ dst.parent.mkdir(parents=True, exist_ok=True)
70
+ out.convert("RGB").save(dst, quality=92)
71
+ print(f"wrote {dst} center=({cx:.0f},{cy:.0f}) hole={hole:.0f} opacity={args.opacity}")
72
+
73
+
74
+ if __name__ == "__main__":
75
+ main()