555 lines
11 KiB
Bash
Executable File
555 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# I'm a bonsai-making machine!
|
|
|
|
#################################################
|
|
##
|
|
# author: John Allbritten
|
|
# my website: theSynAck.com
|
|
#
|
|
# repo: https://gitlab.com/jallbrit
|
|
# script can be found in the bin/bin/fun folder.
|
|
#
|
|
# license: this script is published under GPLv3.
|
|
# I don't care what you do with it, but I do ask
|
|
# that you leave this message please!
|
|
#
|
|
# inspiration: http://andai.tv/bonsai/
|
|
# andai's version was written in JS and served
|
|
# as the basis for this script. Originally, this
|
|
# was just a port.
|
|
##
|
|
#################################################
|
|
|
|
# ------ vars ------
|
|
# CLI options
|
|
|
|
flag_h=false
|
|
live=false
|
|
infinite=false
|
|
|
|
termCols=$(tput cols)
|
|
termRows=$(tput lines)
|
|
geometry="$((termCols - 1)),$termRows"
|
|
|
|
leafchar='&'
|
|
termColors=false
|
|
|
|
message=""
|
|
flag_m=false
|
|
basetype=1
|
|
multiplier=5
|
|
|
|
lifeStart=28
|
|
steptime=0.01 # time between steps
|
|
|
|
# non-CLI options
|
|
lineWidth=4 # words per line
|
|
|
|
# ------ parse options ------
|
|
|
|
OPTS="hlt:ig:c:Tm:b:M:L:" # the colon means it requires a value
|
|
LONGOPTS="help,live,time:,infinite,geo:,leaf:,termcolors,message:,base:,multiplier:,life:"
|
|
|
|
parsed=$(getopt --options=$OPTS --longoptions=$LONGOPTS -- "$@")
|
|
eval set -- "${parsed[@]}"
|
|
|
|
while true; do
|
|
case "$1" in
|
|
-h|--help)
|
|
flag_h=true
|
|
shift
|
|
;;
|
|
|
|
-l|--live)
|
|
live=true
|
|
shift
|
|
;;
|
|
|
|
-t|--time)
|
|
steptime="$2"
|
|
shift 2
|
|
;;
|
|
|
|
-i|--infinite)
|
|
infinite=true
|
|
shift
|
|
;;
|
|
|
|
-g|--geo)
|
|
geo=$2
|
|
shift 2
|
|
;;
|
|
|
|
-c|--leaf)
|
|
leafchar="$2"
|
|
shift 2
|
|
;;
|
|
|
|
-T|--termcolors)
|
|
termColors=true
|
|
shift
|
|
;;
|
|
|
|
-m|--message)
|
|
flag_m=true
|
|
message="$2"
|
|
shift 2
|
|
;;
|
|
|
|
-b|--basetype)
|
|
basetype="$2"
|
|
shift 2
|
|
;;
|
|
|
|
-M|--multiplier)
|
|
multiplier="$2"
|
|
shift 2
|
|
;;
|
|
|
|
-L|--life)
|
|
lifeStart="$2"
|
|
shift 2
|
|
;;
|
|
|
|
--) # end of arguments
|
|
shift
|
|
break
|
|
;;
|
|
|
|
*)
|
|
echo "error while parsing CLI options"
|
|
flag_h=true
|
|
;;
|
|
esac
|
|
done
|
|
|
|
HELP="Usage: bonsai [-h] [-i] [-l] [-T] [-m message] [-t time]
|
|
[-g x,y] [ -c char] [-M 0-9]
|
|
|
|
bonsai.sh is a static and live bonsai tree generator, written in bash.
|
|
|
|
optional args:
|
|
-l, --live enable live generation
|
|
-t, --time time time between each step of growth [default: 0.01]
|
|
-m, --message text attach a message to the tree
|
|
-b, --basetype 0-2 which ascii-art plant base to use (0 for none) [default: 1]
|
|
-i, --infinite keep generating trees until quit (2s between each)
|
|
-T, --termcolors use terminal colors
|
|
-g, --geo geo set custom geometry [default: fit to terminal]
|
|
-c, --leaf char character used for leaves [default: &]
|
|
-M, --multiplier 0-9 branch multiplier; higher equals more branching [default: 5]
|
|
-L, --life int life of tree; higher equals more overall growth [default: 28]
|
|
-h, --help show help"
|
|
|
|
# check for help
|
|
$flag_h && echo -e "$HELP" && exit 0
|
|
|
|
# geometry processing
|
|
cols=$(echo "$geometry" | cut -d ',' -f1) # width; X
|
|
rows=$(echo "$geometry" | cut -d ',' -f2) # height; Y
|
|
|
|
IFS=$'\n' # delimit strings by newline
|
|
tabs 4 # set tabs to 4 spaces
|
|
|
|
declare -A gridMessage
|
|
|
|
# message processing
|
|
if [ $flag_m = true ]; then
|
|
|
|
messageWidth=20
|
|
|
|
# make room for the message to go on the right side
|
|
cols=$((cols - messageWidth - 8 ))
|
|
|
|
# wordwrap message, delimiting by spaces
|
|
message="$(echo "$message" | fold -sw $messageWidth)"
|
|
|
|
# get number of lines in the message
|
|
messageLineCount=0
|
|
for line in $message; do
|
|
messageLineCount=$((messageLineCount + 1))
|
|
done
|
|
|
|
messageOffset=$((rows - messageLineCount - 7))
|
|
|
|
# put lines of message into a grid
|
|
index=$messageOffset
|
|
for line in $message; do
|
|
gridMessage[$index]="$line"
|
|
index=$((index + 1))
|
|
done
|
|
fi
|
|
|
|
# define colors
|
|
if [ $termColors = true ]; then
|
|
LightBrown='\e[1;33m'
|
|
DarkBrown='\e[0;33m'
|
|
BrownGreen='\e[1;32m'
|
|
Green='\e[0;32m'
|
|
else
|
|
LightBrown='\e[38;5;172m'
|
|
DarkBrown='\e[38;5;130m'
|
|
BrownGreen='\e[38;5;142m'
|
|
Green='\e[38;5;106m'
|
|
fi
|
|
Grey='\e[1;30m'
|
|
R='\e[0m'
|
|
|
|
# create ascii base in lines
|
|
base=""
|
|
case $basetype in
|
|
0)
|
|
base="" ;;
|
|
|
|
1)
|
|
width=15
|
|
art="\
|
|
${Grey}:${Green}___________${DarkBrown}./~~\\.${Green}___________${Grey}:
|
|
\\ /
|
|
\\________________________/
|
|
(_) (_)"
|
|
;;
|
|
|
|
2)
|
|
width=7
|
|
art="\
|
|
${Grey}(${Green}---${DarkBrown}./~~\\.${Green}---${Grey})
|
|
( )
|
|
(________)"
|
|
;;
|
|
esac
|
|
|
|
# get base height
|
|
baseHeight=0
|
|
for line in $art; do
|
|
baseHeight=$(( baseHeight + 1 ))
|
|
done
|
|
|
|
# add spaces before base so that it's in the middle of the terminal
|
|
iter=1
|
|
for line in $art; do
|
|
filler=''
|
|
for (( i=0; i < $(( (cols / 2) - width )); i++)); do
|
|
filler+=" "
|
|
done
|
|
base+="${filler}${line}"
|
|
[ $iter -ne $baseHeight ] && base+='\n'
|
|
iter=$((iter+1))
|
|
done
|
|
unset IFS # reset delimiter
|
|
|
|
rows=$((rows - baseHeight))
|
|
|
|
declare -A grid # must be done outside function for unknown reason
|
|
|
|
trap 'echo "press q to quit"' SIGINT # disable CTRL+C
|
|
|
|
init() {
|
|
branches=0
|
|
shoots=0
|
|
|
|
branchesMax=$((multiplier * 110))
|
|
shootsMax=$multiplier
|
|
|
|
# fill grid full of spaces
|
|
for (( row=0; row < $rows; row++ )); do
|
|
for (( col=0; col < $cols; col++ )); do
|
|
grid[$row,$col]=' '
|
|
done
|
|
done
|
|
|
|
# No echo stdin and hide the cursor
|
|
if [ $live = true ]; then
|
|
stty -echo
|
|
echo -ne "\e[?25l"
|
|
|
|
echo -ne "\e[2J"
|
|
fi
|
|
}
|
|
|
|
grow() {
|
|
local start=$((cols / 2))
|
|
|
|
local x=$((cols / 2)) # start halfway across the screen
|
|
local y=$rows # start just above the base
|
|
|
|
branch $x $y trunk $lifeStart
|
|
}
|
|
|
|
branch() {
|
|
# argument declarations
|
|
local x=$1
|
|
local y=$2
|
|
local type=$3
|
|
local life=$4
|
|
local dx=0
|
|
local dy=0
|
|
|
|
# check if the user is hitting q
|
|
timeout=0.001
|
|
[ $live = "false" ] && timeout=.0001
|
|
read -n 1 -t $timeout input
|
|
[ "$input" = "q" ] && clean "quit"
|
|
|
|
branches=$((branches + 1))
|
|
|
|
# as long as we're alive...
|
|
while [ $life -gt 0 ]; do
|
|
|
|
life=$((life - 1)) # ensure life ends
|
|
|
|
# case $life in
|
|
# [0]) type=dead ;;
|
|
# [1-4]) type=dying ;;
|
|
# esac
|
|
|
|
# set dy based on type
|
|
case $type in
|
|
shoot*) # if this is a shoot, trend horizontal/downward growth
|
|
case "$((RANDOM % 10))" in
|
|
[0-1]) dy=-1 ;;
|
|
[2-7]) dy=0 ;;
|
|
[8-9]) dy=1 ;;
|
|
esac
|
|
;;
|
|
|
|
dying) # discourage vertical growth
|
|
case "$((RANDOM % 10))" in
|
|
[0-1]) dy=-1 ;;
|
|
[2-8]) dy=0 ;;
|
|
[9-10]) dy=1 ;;
|
|
esac
|
|
;;
|
|
|
|
*) # otherwise, let it grow up/not at all
|
|
dy=0
|
|
[ $life -ne $lifeStart ] && [ $((RANDOM % 10)) -gt 2 ] && dy=-1
|
|
;;
|
|
esac
|
|
# if we're about to hit the ground, cut it off
|
|
[ $dy -gt 0 ] && [ $y -gt $(( rows - 1 )) ] && dy=0
|
|
[ $type = "trunk" ] && [ $life -lt 4 ] && dy=0
|
|
|
|
# set dx based on type
|
|
case $type in
|
|
shootLeft) # tend left: dx=[-2,1]
|
|
case $(( RANDOM % 10 )) in
|
|
[0-1]) dx=-2 ;;
|
|
[2-5]) dx=-1 ;;
|
|
[6-8]) dx=0 ;;
|
|
[9]) dx=1 ;;
|
|
esac ;;
|
|
|
|
shootRight) # tend right: dx=[-1,2]
|
|
case $(( RANDOM % 10 )) in
|
|
[0-1]) dx=2 ;;
|
|
[2-5]) dx=1 ;;
|
|
[6-8]) dx=0 ;;
|
|
[9]) dx=-1 ;;
|
|
esac ;;
|
|
|
|
dying) # tend left/right: dx=[-3,3]
|
|
dx=$(( (RANDOM % 7) - 3)) ;;
|
|
|
|
*) # tend equal: dx=[-1,1]
|
|
dx=$(( (RANDOM % 3) - 1)) ;;
|
|
|
|
esac
|
|
|
|
# re-branch upon conditions
|
|
if [ $branches -lt $branchesMax ]; then
|
|
|
|
# branch is dead
|
|
if [ $life -lt 3 ]; then
|
|
branch $x $y dead $life
|
|
|
|
# branch is dying and needs to branch into leaves
|
|
elif [ $type = trunk ] && [ $life -lt $((multiplier + 2)) ]; then
|
|
branch $x $y dying $life
|
|
|
|
elif [[ $type = "shoot"* ]] && [ $life -lt $((multiplier + 2)) ]; then
|
|
branch $x $y dying $life
|
|
|
|
# re-branch if: not close to the base AND (pass a chance test OR be a trunk, not have too man shoots already, and not be about to die)
|
|
elif [[ $type = trunk && $life -lt $((lifeStart - 8)) \
|
|
&& ( $(( RANDOM % (16 - multiplier) )) -eq 0 \
|
|
|| ($type = trunk && $(( life % 5 )) -eq 0 && $life -gt 5) ) ]]; then
|
|
|
|
# if a trunk is splitting and not about to die, chance to create another trunk
|
|
if [ $((RANDOM % 3)) -eq 0 ] && [ $life -gt 7 ]; then
|
|
branch $x $y trunk $life
|
|
|
|
elif [ $shoots -lt $shootsMax ]; then
|
|
|
|
# give the shoot some life
|
|
tmpLife=$(( life + multiplier - 2 ))
|
|
[ $tmpLife -lt 0 ] && tmpLife=0
|
|
|
|
# first shoot is randomly directed
|
|
if [ $shoots -eq 0 ]; then
|
|
tmpType=shootLeft
|
|
[ $((RANDOM % 2)) -eq 0 ] && tmpType=shootRight
|
|
|
|
|
|
# secondary shoots alternate from the first
|
|
else
|
|
case $tmpType in
|
|
shootLeft) # last shoot was left, shoot right
|
|
tmpType=shootRight ;;
|
|
shootRight) # last shoot was right, shoot left
|
|
tmpType=shootLeft ;;
|
|
esac
|
|
fi
|
|
branch $x $y $tmpType $tmpLife
|
|
shoots=$((shoots + 1))
|
|
fi
|
|
fi
|
|
else # if we're past max branches but want to branch...
|
|
char='<>'
|
|
fi
|
|
|
|
# implement dx,dy
|
|
x=$((x + dx))
|
|
y=$((y + dy))
|
|
|
|
# choose color
|
|
case $type in
|
|
trunk|shoot*)
|
|
color=${DarkBrown}
|
|
[ $(( RANDOM % 4 )) -eq 0 ] && color=${LightBrown}
|
|
;;
|
|
|
|
dying) color=${BrownGreen} ;;
|
|
|
|
dead) color=${Green} ;;
|
|
esac
|
|
|
|
# choose branch character
|
|
case $type in
|
|
trunk)
|
|
if [ $dx -lt 0 ]; then
|
|
char='\\'
|
|
elif [ $dx -eq 0 ]; then
|
|
char='/|'
|
|
elif [ $dx -gt 0 ]; then
|
|
char='/'
|
|
fi
|
|
[ $dy -eq 0 ] && char='/~' # not growing
|
|
#[ $dy -lt 0 ] && char='/~' # growing
|
|
;;
|
|
|
|
# shoots tend to look horizontal
|
|
shootLeft)
|
|
case $dx in
|
|
[-3,-1]) char='\\|' ;;
|
|
[0]) char='/|' ;;
|
|
[1,3]) char='/' ;;
|
|
esac
|
|
#[ $dy -lt 0 ] && char='/~' # growing up
|
|
[ $dy -gt 0 ] && char='/' # growing down
|
|
[ $dy -eq 0 ] && char='\\_' # not growing
|
|
;;
|
|
|
|
shootRight)
|
|
case $dx in
|
|
[-3,-1]) char='\\|' ;;
|
|
[0]) char='/|' ;;
|
|
[1,3]) char='/' ;;
|
|
esac
|
|
#[ $dy -lt 0 ] && char='' # growing up
|
|
[ $dy -gt 0 ] && char='\\' # growing down
|
|
[ $dy -eq 0 ] && char='_/' # not growing
|
|
;;
|
|
|
|
#dead)
|
|
# #life=$((life + 1))
|
|
# char="${leafchar}"
|
|
# [ $dx -lt -2 ] || [ $dx -gt 2 ] && char="${leafchar}${leafchar}"
|
|
# ;;
|
|
|
|
esac
|
|
|
|
# set leaf if needed
|
|
[ $life -lt 4 ] && char="${leafchar}"
|
|
|
|
# uncomment for help debugging
|
|
#echo -e "$life:\t$x, $y: $char"
|
|
|
|
# put character in grid
|
|
grid[$y,$x]="${color}${char}${R}"
|
|
|
|
# if live, print what we have so far and let the user see it
|
|
if [ $live = true ]; then
|
|
print
|
|
sleep $steptime
|
|
fi
|
|
done
|
|
}
|
|
|
|
print() {
|
|
# parse grid for output
|
|
output=""
|
|
for (( row=0; row < $rows; row++)); do
|
|
|
|
line=""
|
|
|
|
for (( col=0; col < $cols; col++ )); do
|
|
|
|
# this prints a space at 0,0 and is necessary at the moment
|
|
[ $live = true ] && echo -ne "\e[0;0H "
|
|
|
|
# grab the character from our grid
|
|
line+="${grid[$row,$col]}"
|
|
done
|
|
|
|
# add our message
|
|
if [ $flag_m = true ]; then
|
|
# remove trailing whitespace before we add our message
|
|
line=$(sed -r 's/[ \t]*$//' <(printf "$line"))
|
|
line+=" \t${gridMessage[$row]}"
|
|
fi
|
|
|
|
line="${line}\n"
|
|
|
|
# end 'er with the ol' newline
|
|
output+="$line"
|
|
done
|
|
|
|
# add the ascii-art base we generated earlier
|
|
output+="$base"
|
|
|
|
# output, removing trailing whitespace
|
|
sed -r 's/[ \t]*$//' <(printf "$output")
|
|
}
|
|
|
|
clean() {
|
|
# Show cursor and echo stdin
|
|
if [ $live = true ]; then
|
|
echo -ne "\e[?25h"
|
|
stty echo
|
|
fi
|
|
|
|
echo "" # ensure the cursor resets to the next line
|
|
|
|
# if we wanna quit
|
|
if [ "$1" = "quit" ]; then
|
|
trap SIGINT
|
|
exit 0
|
|
fi
|
|
}
|
|
|
|
bonsai() {
|
|
init
|
|
grow
|
|
print
|
|
clean
|
|
}
|
|
|
|
bonsai
|
|
|
|
while [ $infinite = true ]; do
|
|
sleep 2
|
|
bonsai
|
|
done
|