| 1 | #!/usr/bin/env bash |
| 2 | |
| 3 | set -euo pipefail |
| 4 | |
| 5 | INPUT_STRING="${1:?Usage: $0 <seed_string> <result.png>}" |
| 6 | OUTPUT="${2:?Usage: $0 <seed_string> <result.png>}" |
| 7 | |
| 8 | if [[ ! -f "$OUTPUT" ]]; then |
| 9 | echo "Error: '$OUTPUT' not found." >&2 |
| 10 | exit 1 |
| 11 | fi |
| 12 | |
| 13 | read -r IMG_W IMG_H < <( |
| 14 | ffprobe -v error \ |
| 15 | -select_streams v:0 \ |
| 16 | -show_entries stream=width,height \ |
| 17 | -of csv=p=0 \ |
| 18 | "$OUTPUT" | tr ',' ' ' |
| 19 | ) |
| 20 | |
| 21 | if [[ -z "$IMG_W" || -z "$IMG_H" ]]; then |
| 22 | echo "Error: could not read dimensions from '$OUTPUT'." >&2 |
| 23 | exit 1 |
| 24 | fi |
| 25 | |
| 26 | HASH=$(printf '%s' "$INPUT_STRING" | sha256sum | awk '{print $1}') |
| 27 | |
| 28 | chunk1=$(( 16#${HASH:0:8} )) |
| 29 | chunk2=$(( 16#${HASH:8:8} )) |
| 30 | chunk3=$(( 16#${HASH:16:8} )) |
| 31 | chunk4=$(( 16#${HASH:24:8} )) |
| 32 | |
| 33 | R=$(( chunk1 % 256 )) |
| 34 | G=$(( chunk2 % 256 )) |
| 35 | B=$(( chunk3 % 256 )) |
| 36 | |
| 37 | ROTATION=$(( chunk4 % 360 )) |
| 38 | |
| 39 | R2=$(( (chunk1 / 256) % 256 )) |
| 40 | G2=$(( (chunk2 / 256) % 256 )) |
| 41 | B2=$(( (chunk3 / 256) % 256 )) |
| 42 | |
| 43 | STRLEN=${#INPUT_STRING} |
| 44 | TILE_SIZE=$(( 64 + (STRLEN % 192) )) |
| 45 | |
| 46 | GEQ_R="between(mod((X+Y),${TILE_SIZE}),0,$(( TILE_SIZE / 2 )))*${R} + between(mod((X+Y),${TILE_SIZE}),$(( TILE_SIZE / 2 + 1 )),${TILE_SIZE})*${R2}" |
| 47 | GEQ_G="between(mod((X+Y),${TILE_SIZE}),0,$(( TILE_SIZE / 2 )))*${G} + between(mod((X+Y),${TILE_SIZE}),$(( TILE_SIZE / 2 + 1 )),${TILE_SIZE})*${G2}" |
| 48 | GEQ_B="between(mod((X+Y),${TILE_SIZE}),0,$(( TILE_SIZE / 2 )))*${B} + between(mod((X+Y),${TILE_SIZE}),$(( TILE_SIZE / 2 + 1 )),${TILE_SIZE})*${B2}" |
| 49 | |
| 50 | ROTATION_RAD=$(awk "BEGIN { printf \"%.6f\", ${ROTATION} * 3.14159265358979 / 180 }") |
| 51 | |
| 52 | TMPFILE=$(mktemp "${OUTPUT}.tmp.XXXXXX.png") |
| 53 | trap 'rm -f "$TMPFILE"' EXIT |
| 54 | |
| 55 | ffmpeg -y \ |
| 56 | -f lavfi -i "nullsrc=size=${IMG_W}x${IMG_H}:rate=1" \ |
| 57 | -i "$OUTPUT" \ |
| 58 | -filter_complex " |
| 59 | [0:v]geq= |
| 60 | r='${GEQ_R}': |
| 61 | g='${GEQ_G}': |
| 62 | b='${GEQ_B}': |
| 63 | a=255, |
| 64 | format=rgba[bg]; |
| 65 | |
| 66 | [1:v]format=rgba, |
| 67 | geq= |
| 68 | r='r(X,Y)': |
| 69 | g='g(X,Y)': |
| 70 | b='b(X,Y)': |
| 71 | a='if(gte(r(X,Y)+g(X,Y)+b(X,Y),720),0,if(lte(r(X,Y)+g(X,Y)+b(X,Y),15)*gte(alpha(X,Y),250),0,alpha(X,Y)))', |
| 72 | rotate= |
| 73 | angle=${ROTATION_RAD}: |
| 74 | fillcolor=none: |
| 75 | ow=rotw(${ROTATION_RAD}): |
| 76 | oh=roth(${ROTATION_RAD})[ovr]; |
| 77 | |
| 78 | [bg][ovr]overlay= |
| 79 | x=(W-w)/2: |
| 80 | y=(H-h)/2: |
| 81 | format=auto[out] |
| 82 | " \ |
| 83 | -map "[out]" \ |
| 84 | -frames:v 1 \ |
| 85 | -update 1 \ |
| 86 | "$TMPFILE" |
| 87 | |
| 88 | mv "$TMPFILE" "$OUTPUT" |