fun/bin/bonsai
#!/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 "" in
-h|--help)
flag_h=true
shift
;;
-l|--live)
live=true
shift
;;
-t|--time)
steptime=""
shift 2
;;
-i|--infinite)
infinite=true
shift
;;
-g|--geo)
geo=
shift 2
;;
-c|--leaf)
leafchar=""
shift 2
;;
-T|--termcolors)
termColors=true
shift
;;
-m|--message)
flag_m=true
message=""
shift 2
;;
-b|--basetype)
basetype=""
shift 2
;;
-M|--multiplier)
multiplier=""
shift 2
;;
-L|--life)
lifeStart=""
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=
local y=
local type=
local life=
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 [ "" = "quit" ]; then
trap SIGINT
exit 0
fi
}
bonsai() {
init
grow
print
clean
}
bonsai
while [ $infinite = true ]; do
sleep 2
bonsai
done