Skip to content

Commit 7b24788

Browse files
A Dynamic Collection of Shell Scripts with Educational Purpose
1 parent 9ea6acb commit 7b24788

2 files changed

Lines changed: 652 additions & 0 deletions

File tree

scripts/fog-create

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
#!/usr/bin/env bash
2+
# License: GPLv3
3+
# Credits: Felipe Facundes
4+
5+
# =============================================================================
6+
# fog-create — Procedural fog/smoke generator with transparent background
7+
# Usage:
8+
# fog-create <WxH> [hex_color]
9+
# fog-create 1920x1080
10+
# fog-create 1920x1080 ffffff
11+
# fog-create 1920x1080 "#ffffff"
12+
# =============================================================================
13+
14+
set -euo pipefail
15+
16+
# =============================================================================
17+
# Isolated Python environment (venv) — installed once in ~/.python/
18+
# =============================================================================
19+
py_venv="${HOME}/.python/fog-create-venv"
20+
activate="${py_venv}/bin/activate"
21+
py_bin="${py_venv}/bin/python"
22+
23+
if [[ ! -d "${py_venv}" ]]; then
24+
echo "Setting up isolated Python environment (first run)..."
25+
26+
# Ensures parent directory exists
27+
mkdir -p "${HOME}/.python"
28+
29+
# Creates venv with available Python interpreter
30+
if command -v python3 &>/dev/null; then
31+
python3 -m venv "${py_venv}"
32+
elif command -v python &>/dev/null; then
33+
python -m venv "${py_venv}"
34+
else
35+
echo "Error: no Python interpreter found (python3 or python)." >&2
36+
exit 1
37+
fi
38+
39+
# Activates and installs dependencies
40+
# shellcheck source=/dev/null
41+
source "${activate}"
42+
export VIRTUAL_ENV="${py_venv}"
43+
44+
echo "Installing dependencies (numpy, pillow, scipy)..."
45+
pip install --quiet --upgrade pip
46+
pip install --quiet numpy pillow scipy
47+
48+
echo "Environment configured at: ${py_venv}"
49+
echo ""
50+
else
51+
# Venv already exists — just activate
52+
# shellcheck source=/dev/null
53+
source "${activate}"
54+
export VIRTUAL_ENV="${py_venv}"
55+
fi
56+
57+
# ---------- help -----------------------------------------------------------
58+
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
59+
cat <<'HELP'
60+
fog-create — generates procedural fog (PNG with transparent background)
61+
62+
Usage:
63+
fog-create <WxH> [hex_color]
64+
65+
Examples:
66+
fog-create 1920x1080
67+
fog-create 800x600 ccddff
68+
fog-create 512x512 "#ff8800"
69+
70+
Arguments:
71+
WxH Image dimensions (e.g., 1920x1080)
72+
hex_color Fog color in hex, with or without "#" (default: ffffff)
73+
74+
Output:
75+
fog_<W>x<H>_<color>_<timestamp>.png in current directory
76+
HELP
77+
exit 0
78+
fi
79+
80+
# ---------- arguments ------------------------------------------------------
81+
if [[ $# -lt 1 ]]; then
82+
echo "Usage: fog-create <WxH> [hex_color]" >&2
83+
echo " fog-create --help for more details" >&2
84+
exit 1
85+
fi
86+
87+
DIMENSIONS="${1}"
88+
COLOR_RAW="${2:-ffffff}"
89+
90+
# Validates dimension
91+
if ! [[ "$DIMENSIONS" =~ ^[0-9]+[xX][0-9]+$ ]]; then
92+
echo "Error: invalid dimension '$DIMENSIONS'. Use WxH format, e.g., 1920x1080" >&2
93+
exit 1
94+
fi
95+
96+
WIDTH="${DIMENSIONS%%[xX]*}"
97+
HEIGHT="${DIMENSIONS##*[xX]}"
98+
99+
# Normalizes color (removes # and converts to lowercase)
100+
COLOR_HEX="${COLOR_RAW//\#/}"
101+
COLOR_HEX="${COLOR_HEX,,}"
102+
103+
# Validates hex
104+
if ! [[ "$COLOR_HEX" =~ ^[0-9a-f]{3}([0-9a-f]{3})?$ ]]; then
105+
echo "Error: invalid hex color '$COLOR_RAW'. Use 3 or 6 hex digits, e.g., ffffff or ccddff" >&2
106+
exit 1
107+
fi
108+
109+
# Expands short hex (#abc → aabbcc)
110+
if [[ ${#COLOR_HEX} -eq 3 ]]; then
111+
r="${COLOR_HEX:0:1}"; g="${COLOR_HEX:1:1}"; b="${COLOR_HEX:2:1}"
112+
COLOR_HEX="${r}${r}${g}${g}${b}${b}"
113+
fi
114+
115+
# Output file
116+
TIMESTAMP=$(date +%Y%m%d_%H%M%S_%N | cut -c1-22)
117+
OUTFILE="fog_${WIDTH}x${HEIGHT}_${COLOR_HEX}_${TIMESTAMP}.png"
118+
119+
echo "Generating procedural fog..."
120+
echo " Dimension : ${WIDTH}x${HEIGHT}"
121+
echo " Color : #${COLOR_HEX}"
122+
echo " Output : ${OUTFILE}"
123+
124+
# ---------- generation in Python (uses venv interpreter) -------------------
125+
"${py_bin}" - "$WIDTH" "$HEIGHT" "$COLOR_HEX" "$OUTFILE" <<'PYEOF'
126+
import sys, time
127+
import numpy as np
128+
from PIL import Image
129+
from scipy.ndimage import gaussian_filter
130+
131+
W = int(sys.argv[1])
132+
H = int(sys.argv[2])
133+
color = sys.argv[3]
134+
outf = sys.argv[4]
135+
136+
R = int(color[0:2], 16)
137+
G = int(color[2:4], 16)
138+
B = int(color[4:6], 16)
139+
140+
rng = np.random.default_rng(int(time.time_ns()) & 0xFFFFFFFFFFFFFFFF)
141+
142+
# =============================================================================
143+
# Memory-efficient fBm:
144+
# Noise generated at final size and smoothed with progressive Gaussian blur.
145+
# Large sigma = large structures (low frequency)
146+
# Small sigma = fine details (high frequency)
147+
# =============================================================================
148+
def make_fbm(W, H, octaves=8, base_sigma_frac=0.12, gain=0.50, lacunarity=2.0):
149+
total = np.zeros((H, W), dtype=np.float32)
150+
amp = 1.0
151+
sigma = max(4.0, min(W, H) * base_sigma_frac)
152+
153+
for _ in range(octaves):
154+
layer = rng.standard_normal((H, W)).astype(np.float32)
155+
if sigma >= 1.0:
156+
layer = gaussian_filter(layer, sigma=sigma)
157+
total += amp * layer
158+
amp *= gain
159+
sigma /= lacunarity
160+
161+
lo, hi = total.min(), total.max()
162+
total = (total - lo) / (hi - lo + 1e-9)
163+
return total
164+
165+
# Three layers with random sigmas
166+
s1 = float(rng.uniform(0.10, 0.22))
167+
s2 = float(rng.uniform(0.04, 0.10))
168+
s3 = float(rng.uniform(0.01, 0.04))
169+
170+
fbm1 = make_fbm(W, H, octaves=int(rng.integers(6, 9)), base_sigma_frac=s1)
171+
fbm2 = make_fbm(W, H, octaves=int(rng.integers(5, 8)), base_sigma_frac=s2)
172+
fbm3 = make_fbm(W, H, octaves=int(rng.integers(4, 7)), base_sigma_frac=s3)
173+
174+
w1 = float(rng.uniform(0.50, 0.70))
175+
w2 = float(rng.uniform(0.15, 0.30))
176+
w3 = 1.0 - w1 - w2
177+
fog = w1 * fbm1 + w2 * fbm2 + w3 * fbm3
178+
del fbm1, fbm2, fbm3
179+
180+
fog -= fog.min(); fog /= (fog.max() + 1e-9)
181+
182+
# =============================================================================
183+
# Sigmoid curve → alpha
184+
# =============================================================================
185+
mid = float(rng.uniform(0.32, 0.58))
186+
steep = float(rng.uniform(6.0, 13.0))
187+
188+
# sigmoid without overflow: clip exponent
189+
exponent = np.clip(-steep * (fog - mid), -88, 88).astype(np.float32)
190+
alpha = (1.0 / (1.0 + np.exp(exponent))).astype(np.float32)
191+
del fog, exponent
192+
193+
# Light smoothing
194+
alpha = gaussian_filter(alpha, sigma=max(1.0, min(W, H) * 0.003))
195+
196+
# Gamma
197+
gamma = float(rng.uniform(0.65, 1.05))
198+
np.power(alpha, gamma, out=alpha)
199+
200+
# Contrast redistribution
201+
p_lo = float(np.percentile(alpha, 4))
202+
p_hi = float(np.percentile(alpha, 96))
203+
if p_hi > p_lo:
204+
alpha = (alpha - p_lo) / (p_hi - p_lo)
205+
np.clip(alpha, 0.0, 1.0, out=alpha)
206+
207+
# =============================================================================
208+
# Cosine vignette on edges
209+
# =============================================================================
210+
fade_w = float(rng.uniform(0.05, 0.16))
211+
212+
def edge_curve(n, width):
213+
v = np.linspace(0.0, 1.0, n, dtype=np.float32)
214+
t = np.where(v < width, v / width,
215+
np.where(v > 1.0 - width, (1.0 - v) / width, 1.0))
216+
return ((1.0 - np.cos(t * np.pi)) * 0.5).astype(np.float32)
217+
218+
vx = edge_curve(W, fade_w) # (W,)
219+
vy = edge_curve(H, fade_w)[:, None] # (H,1)
220+
vignette = vx[None, :] * vy # broadcast (H,W)
221+
vignette = gaussian_filter(vignette, sigma=max(2.0, min(W, H) * 0.008))
222+
223+
alpha *= vignette
224+
del vignette
225+
np.clip(alpha, 0.0, 1.0, out=alpha)
226+
227+
# =============================================================================
228+
# Assembles RGBA and saves
229+
# =============================================================================
230+
alpha_u8 = (alpha * 255).astype(np.uint8)
231+
del alpha
232+
233+
rgba = np.stack([
234+
np.full((H, W), R, dtype=np.uint8),
235+
np.full((H, W), G, dtype=np.uint8),
236+
np.full((H, W), B, dtype=np.uint8),
237+
alpha_u8
238+
], axis=-1)
239+
del alpha_u8
240+
241+
Image.fromarray(rgba, "RGBA").save(outf, "PNG")
242+
print(f"ok:{outf}")
243+
PYEOF
244+
245+
# ---------- checks output --------------------------------------------------
246+
if [[ -f "$OUTFILE" ]]; then
247+
SIZE=$(du -sh "$OUTFILE" | cut -f1)
248+
echo ""
249+
echo "Fog generated successfully!"
250+
echo " File : $OUTFILE"
251+
echo " Size : $SIZE"
252+
else
253+
echo "Failed to generate image." >&2
254+
exit 1
255+
fi

0 commit comments

Comments
 (0)