四 花式清屏

四 花式清屏

【动机】

众所周知, 清屏采用 clear 命令即可. 只不过有时为了做一些后台进程加载, 或者纯粹为了提升一些用户体验, 可能需要将清屏的过程做一些修饰.

【意图】

以屏幕每个显示位置(注意不是像素模式下的点, 而是字符模式下的显示单元)为单位, 逐个擦除, 或者配合多个后台进程模拟从不同方位, 同时擦除, 擦除前可能需要将屏幕先涂色.

【效果】

先看看效果:

清屏涉及到一些计算, 所以现在在 data.sh 中, 准备几个函数

【预备函数 index_in_strings】

该函数用于获取字符串在字符串数组中的索引, 可放在 data.sh 中

:<<COMMENT
    查找指定字符串, 在后续的字符串系列中的序号
    $1: 要查找的字符串
    $2 及以后: 在该字符串系列中查找.
    返回值: 该元素在该字符串系列中的序号, 0 是第一个元素. -1 代表找不到
    用法:
        1. 在单个字符串中搜索, 该字符串可能包含多个子串(多个子串由空格分隔):
                index_in_strings "$search" "$string"
        2. 在元素可能存在空格的字符串数组中搜索
                index_in_strings "$search" "${arr[@]}"       
        3. 将字符串数组中的元素, 逐个作为参数
                index_in_strings "$search" $one $two $three ...
COMMENT
index_in_strings(){
    local search=$1
    shift 1
    local curr=0 find=-1
    local arr=("$@")
    for s in "${arr[@]}";do 
        [ "$s" == "$search" ] && find=$curr && break
        curr=$[curr+1]
    done
    echo $find
}

测试 index_in_strings 函数:

  # 根据实际文件位置修改路径
    # source ./data.sh
    way1(){

        array2string(){
            local idx=0
            for idx in ${!strs[@]};do
                printf "[%s]=(%s) " $idx "${strs[idx]}"
            done
            # 去掉最后一个不必要的空格
            [ $idx -ne 0 ] && printf "\b" || :
        }

        local idx search 
        tip "1. 在单个字符串中搜索, 该字符串可能包含多个子串"
        local searchs="one two three four five"
        local strs="One   two   Three four  five six"
        notice "$strs"
        idx=-10
        for search in $searchs; do
            idx=$(index_in_strings "$search" $strs)
            info "<$search> index: $idx"
        done

        tip "2. 在元素可能存在空格的字符串数组中搜索"
        local searchs=('hello' 'hello  world' 'hello world' 'hey you me' 'smile' 'no-such-string' 'sit down please')
        local strs=('one two' ' hey you me' 'smile' 'hey you me' 'hello world' 'sit down')     
        notice "$(array2string)"   
        idx=-10
        for search in "${searchs[@]}"; do
            idx=$(index_in_strings "$search" "${strs[@]}")
            info "<$search> index: $idx"
        done

        tip "3. 将字符串数组中的元素, 逐个作为参数, 效果与 2 相同"
        local searchs=('hello' 'hello  world' 'hello world' 'hey you me' 'smile' 'no-such-string' 'sit down please')  
        idx=-10
        for search in "${searchs[@]}"; do
            idx=$(index_in_strings "$search" 'one two' ' hey you me' 'smile' 'hey you me' 'hello world' 'sit down')
            info "<$search> index: $idx"
        done     
    }
    way1
    unset -f way1

测试结果如下:

【预备函数 ensure_integer 】

该函数确保给定的数据为指定的整数(一般在此处填入默认值), 可放在 data.sh 中.

:<<COMMENT
    确保给定的数据, 是整数, 包括正整数, 负整数和 0
    $1: $2不合格时, 返回此默认值
    $2: 要考察的数据
    使用: ensure_integer <default-integer> <datastring>
    返回值: 优先考虑给定数据, 不合格则返回默认值
COMMENT
ensure_integer(){
    [ -z "$1" ] && error "未指定默认值" && exit 1
    [[ $2 =~ ^[+-]?[0-9]+$ ]] && echo $2 || echo $1    
}

该函数非常简单, 目的只在于避免调用函数中重复出现正则表达式. 测试略.

【预备函数 random_from_range】

该函数返回给定边界的整数值, 此处利用了系统预设变量 RANDOM , 它随机返回0至32767的整数.

:<<COMMENT
    根据指定范围, 获取随机数
    $1: bound1 default to 1
    $2: bound2 default to 100
    用法: random_from_range <bound1> <bound2>
    注意: 边界值顺序不要求; 返回的随机值包含边界值
COMMENT
random_from_range(){
    local start=$(ensure_integer 1 $1)
    local end=$(ensure_integer 100 $2)

    # 边界值修正
    if [[ $start -gt $end ]]; then
        let start+=end
        let end=start-end
        let start=start-end
    fi 
    echo $((RANDOM % (end-start+1) + start))
}

测试如下:

    # 测试 random_from_range
    # 根据实际文件位置修改路径
    # source ./data.sh    
    way3(){
        echo
        local i
        for i in $(seq 1 50); do
            printf "%d " $(random_from_range 6 -6)
        done
        echo
    }
    way3
    unset -f way3

测试结果

可见, 边界可为任意整数(正负以及0), 且边界顺序不作要求.

【预备函数 random_from_strings】

从给定的字符串系列中, 随机返回一个作为结果.

:<<COMMENT
    从给定的字符串列表中, 随机返回一个字符串
    $@: 字符串列表, 空则返回空
    用法:
        1. 参数以单个字符串中表示, 该字符串可能包含多个子串(多个子串由空格分隔):
                random_from_strings  "$string"
        2. 在元素可能存在空格的字符串数组中搜索
                random_from_strings  "${arr[@]}"       
        3. 将字符串数组中的元素, 逐个作为参数
                random_from_strings  "$one" "$two" "$three" ...
COMMENT
random_from_strings(){
    if [ $# -ge 1 ]; then
        local idx=$(random_from_range 1 $#)
        echo ${!idx}
    fi
}

【预备函数 draw_rect】

绘制指定区域, 文本可选. 注意此处未采用常见的 x,y,width,height 形式的参数. 默认区域为全屏.

:<<COMMENT
    垂直方向, 逐行绘制矩形,  所有参数可选
    $1: left_x, 矩形左侧的 x 坐标(起点终点相同), 代表屏幕左上角向右第几列(边沿为 1), default to 1 (x)
    $2: start_y, 起点 y 坐标, 代表屏幕左上角向下第几行(边沿为 1), default to 1 (y)
    $3: end_y, 终点 y 坐标, 计算方式同 y1, default to the last line (y2)
    $4: w, 矩形的宽度(指的是屏幕占列数), default to the screen width
    $5: clr_str, default to '32;45'
    $6: text, 要居中显示的文本. 左右上下尽量居中
    $7: all, 任意字符串, 表示文本在每行都显示. 默认(包括空)是仅仅是中间一行
    用法: draw_rect <x> <start_y> <end_y> <width> <clr_str> <text> <all>
COMMENT
draw_rect(){
    local left_x=${1:-1}
    local start_y=${2:-1}
    local end_y=${3:-$(tput lines)}
    local w=${4:-$(tput cols)}
    local clr_str=${5:-'32;45'}

    local y_text=$[($end_y+$start_y)/2]
    local step=1
    [ $start_y -gt $end_y ] && step=-1

    local y ptail
    for y in $(seq $start_y $step $end_y); do
        ptail=($w $clr_str  $y $left_x)
        [[ "$6" && ( "$7" || $y == $y_text ) ]] && draw_text_center "$6" ${ptail[@]} \
            || draw_spaces ${ptail[@]}
    done
}

【预备函数 paint_unit】

给屏幕的一个指定横纵坐标的位置染色, 位于 console.sh

:<<COMMENT
    给屏幕的一个指定横纵坐标的位置染色. 注意坐标值是从 1 算起. 即第一行(列)的索引为 1
    $1: x   index of column( not nullable )
    $2: y   index of line( not nullable )
    $3: clr_str  color string
    用法: paint_unit <x> <y> <clr_str>
COMMENT
paint_unit(){
    local clr_str=${3:-"0"}
    printf   "\033[$2;$1H\e[${clr_str}m%1s\033[0m"
}

很明显, 函数的目的主要还是避免在调用方重复书写位置/颜色格式

【预备函数 paint_rect】

在指定区域涂色, ,位于 console.sh

:<<COMMENT
    涂色指定区域, 默认逐列, 若要逐行, 必须明确指定为 by_col
    $1: x1: 起点 x 坐标 ( not nullable )
    $2: y1: 起点 y 坐标 ( not nullable )
    $3: x2: 终点 x 坐标 ( not nullable )
    $4: y2: 终点 y 坐标 ( not nullable )
    $5: step: 步进值 ( nullable, default to 1)
    $6: style 涂色方式 ( nullable, default to by_col)
        1) by_row: 逐行
        2) 其他(包括空): 逐列(by_col)
    $7: zigzag 是否启用锯齿 ( nullable, default to false)
        1) true: 是
        2) 其他(包括空): 否
    $8: clr_str
    用法: paint_rect x1=0 y1=0 x2=cols y2=lines <step=1> \
            <style=by_col> <zigzag=false> <clr_str>
COMMENT
paint_rect(){
    local cols=$(tput cols) lines=$(tput lines)  
    local x1=$(ensure_natural 0 $1)
    local y1=$(ensure_natural 0 $2)
    local x2=$(ensure_natural $[cols-1] $3)
    local y2=$(ensure_natural $[lines-1] $4)

    local step=$(ensure_natural 1 $5)
    local x_step=$step  y_step=$step  
    [ $x1 -gt $x2 ] && let x_step*=-1
    [ $y1 -gt $y2 ] && let y_step*=-1

    # 默认逐列
    local inner_start=$y1 inner_step=$y_step inner_end=$y2 # 先变化, 位于内层循环
    local outer_start=$x1 outer_step=$x_step outer_end=$x2 # 后变化,
    
    # 修改为逐行
    [ "$6" == "by_row" ] && \
        inner_start=$x1 && inner_step=$x_step && inner_end=$x2 && \
        outer_start=$y1 && outer_step=$y_step && outer_end=$y2   

    #
    #local bclr=$(ensure_natural 0 $8) fclr=$(ensure_natural 0 $9)
    local clr_str=${8:-"0"}

    local d=0 sign
    local coord
    for outer in $(seq $outer_start $outer_step $outer_end); do
        sleep 0.01
        if [ "$7" == true ]; then
            d=$[ outer - outer_start ]
            d=$[ d > 0 ? d : -d ]
            sign=$[ inner_step > 0 ? 1 : -1 ]
            d=$[d*sign]
        fi
        for inner in $(seq $[inner_start+d] $inner_step $inner_end); do           
             [ "$6" == "by_row" ] && coord="$inner $outer" || coord="$outer $inner"
            paint_unit $coord $clr_str
            sleep 0.002
        done
    done
}

由于函数同时考虑了涂色方式(逐行/逐列), 是否启用锯齿的选择, 所以存在比较多的判断, 而这也是代码稍长的原因. 这样做的好处是, 下面的 erase_screen 调用起来会非常的简单而整齐.

【核心函数 erase_screen】

清屏, 唯一参数是擦除方式, 目前一共九种, 随机或者书写错误的方式, 从这九种方式中产生. 该函数位于 console.sh

:<<COMMENT
自定义清屏
    $@: erase_style 擦除方式
        0. left to right: 左边到右边
        1. right to left: 右边到左边
        2. center to sides:  中间到左右两边
        3. top to bottom: 上边到下边
        4. bottom to top: 下边到上边
        5. middle to sides:  中间到上下两边
        6. center to corner:  中间到四个角
        7. corner to center:  四个角到中间 
        8. louver
        default: random 以上几种方式随机选其一
    用法: erase screen <erase style?>
COMMENT
erase_screen(){  
    local styles=(
        'left to right'           # index 0
        'right to left' 
        'center to sides'
        'top to bottom' 
        'bottom to top' 
        'middle to sides'
        'center to corner'
        'corner to center'
        'louver')                 # index=8

    local rdm_sty=$(random_from_strings "${styles[@]}")
    local sty=$(available_range "$@" "$rdm_sty" "${styles[@]}")
    log_info "$sty"

    local lines=$(tput lines) cols=$(tput cols)

    local start_x=1                 start_y=1
    local end_x=$cols               end_y=$lines 

    local mid=$[cols/2+1]             ct=$[lines/2+1] 
    local start_x_1=$[start_x+1]    start_y_1=$[start_y+1]
    local end_x_1=$[end_x+1]        end_y_1=$[end_y+1] 

    local left_top=" $start_x $start_y "      middle_top=" $mid $start_y "       right_top=" $end_x $start_y "
    local left_middle=" $start_x $ct " middle_center=" $mid $ct "  right_middle=" $end_x $ct "   
    local left_bottom=" $start_x $end_y "  middle_bottom=" $mid $end_y "   right_bottom=" $end_x $end_y"
    local left_top_1=" $start_x_1 $start_y_1 " right_bottom_1=" $end_x_1 $end_y_1  "

    tput init
    tput civis  # 隐藏光标
    case "$sty" in
        'left to right') 
            paint_rect $left_top $right_bottom ;; 
        'right to left') 
            paint_rect $right_top $left_bottom  ;;
        'center to sides') 
            paint_rect $middle_top $right_bottom &
            paint_rect $middle_top $left_bottom  ;;
        'top to bottom') 
            paint_rect $left_top $right_bottom 1 by_row;;
        'bottom to top') 
            paint_rect $left_bottom $right_top 1 by_row;;
        'middle to sides') 
            paint_rect $left_middle $right_top 1 by_row &
            paint_rect $right_middle $left_bottom 1 by_row ;;
        "center to corner") 
            paint_rect $middle_center $right_top 1 by_row true &        
            paint_rect $middle_center $left_top 1 by_col true &
            paint_rect $middle_center $right_bottom 1 by_col true &
            paint_rect $middle_center $left_bottom 1 by_row true &
            wait
            paint_rect $middle_center $right_top 1 by_col true &
            paint_rect $middle_center $left_top 1 by_row true &
            paint_rect $middle_center $right_bottom 1 by_row true &         
            paint_rect $middle_center $left_bottom 1 by_col true & ;;  
        'corner to center') 
            paint_rect $right_top $middle_center 1 by_row true &  
            paint_rect $right_top $middle_center 1 by_col true & 
            paint_rect $left_top $middle_center 1 by_col true &
            paint_rect $left_top $middle_center 1 by_row true &
            paint_rect $right_bottom $middle_center 1 by_col true &
            paint_rect $left_bottom $middle_center 1 by_row true &
            paint_rect $right_bottom $middle_center 1 by_row true &         
            paint_rect $left_bottom  $middle_center 1 by_col true & ;;  
        'louver') 
            paint_rect $left_top $right_bottom 2 by_row & 
            paint_rect $left_top_1 $right_bottom  2 by_col & 
            wait

            paint_rect $middle_center $right_top 1 by_row true &
            paint_rect $middle_center $right_bottom 1 by_col true & 
            paint_rect $middle_center $left_bottom 1 by_row true & 
            paint_rect $middle_center $left_top 1 by_col true & 
            wait

            paint_rect $middle_center $right_top 1 by_col true & 
            paint_rect $middle_center $right_bottom 1 by_row true &  
            paint_rect $middle_center $left_bottom 1 by_col true & 
            paint_rect $middle_center $left_top 1 by_row true & 
            ;;
        *) log_error "unknow error...";;
    esac
    wait
    tput cnorm # 恢复光标
}

代码虽然稍微有点长, 还是比较简单, 大致是: 确定方式->确定起始位置->擦除

【测试】

在 test.sh 中, 输入以下代码:

    # 根据实际文件位置修改路径
    # source ./data.sh
    # source ./console.sh
    effect(){      
        local -A styles=(
            ['left to right']="41;33"
            ['right to left']="42;34"
            ['center to sides']="43;35"
            ['top to bottom']="44;36"
            ['bottom to top']="45;37"
            ['middle to sides']="46;31"
            ["center to corner"]="42;35"
            ['corner to center']="43;31"
            ['louver']="45;36"
            ['random']="42;35"
            )

        local lines=$(tput lines) cols=$(tput cols)
        local y=$[lines/2] key clr_str
        for key in "${!styles[@]}"; do
            clr_str=${styles["$key"]}
            draw_text_center "$key" $cols $clr_str $y 1
            sleep 1.5
            draw_rect  '' '' '' '' $clr_str
            erase_screen "$key"
        done
    }
    effect 
    unset -f effect

运行结果见开篇视频.

以上测试代码比较长, 主要是为了展示所有的清屏 style. 正常使用, 一般如下即可(以百叶窗为例):

    # 根据实际文件位置修改路径
    # source ./data.sh
    # source ./console.sh
    # 必要时, 先全屏纯色
    draw_rect
    erase_screen louver # 或者采用随机, 不带参数: erase_screen

谢谢观看.