187 lines
4.3 KiB
Crystal
187 lines
4.3 KiB
Crystal
require "option_parser"
|
|
|
|
BASE_SHAPE = [
|
|
" # ",
|
|
" ### ",
|
|
" ##### ",
|
|
" ####### ",
|
|
" ######### ",
|
|
" ####### ",
|
|
" ##### ",
|
|
" ### ",
|
|
" # ",
|
|
]
|
|
|
|
struct TermSize
|
|
getter cols : Int32
|
|
getter rows : Int32
|
|
def initialize(@cols : Int32, @rows : Int32); end
|
|
end
|
|
|
|
def terminal_size : TermSize
|
|
if STDOUT.tty?
|
|
if (cols_env = ENV["COLUMNS"]?) && (rows_env = ENV["LINES"]?)
|
|
cols = cols_env.to_i
|
|
rows = rows_env.to_i
|
|
return TermSize.new(cols: cols, rows: rows) if cols > 0 && rows > 0
|
|
end
|
|
buf = IO::Memory.new
|
|
status = Process.run("stty", ["size"], output: buf, input: Process::Redirect::Close, error: Process::Redirect::Close)
|
|
if status.success?
|
|
parts = buf.to_s.strip.split(/\s+/)
|
|
if parts.size == 2
|
|
rows = parts[0].to_i
|
|
cols = parts[1].to_i
|
|
return TermSize.new(cols: cols, rows: rows) if cols > 0 && rows > 0
|
|
end
|
|
end
|
|
end
|
|
TermSize.new(cols: 80, rows: 24)
|
|
end
|
|
|
|
def enter_alt(io : IO)
|
|
io << "\e[?1049h\e[?25l"
|
|
end
|
|
|
|
def leave_alt(io : IO)
|
|
io << "\e[0m\e[?25h\e[?1049l"
|
|
end
|
|
|
|
def move_cursor(io : IO, row : Int32, col : Int32)
|
|
io << "\e[#{row};#{col}H"
|
|
end
|
|
|
|
def scale_shape(shape : Array(String), factor : Int32) : Array(String)
|
|
return shape if factor <= 1
|
|
scaled = [] of String
|
|
shape.each do |row|
|
|
expanded = String.build do |io|
|
|
row.each_char do |ch|
|
|
fill = ch == ' ' ? ' ' : '#'
|
|
factor.times { io << fill }
|
|
end
|
|
end
|
|
factor.times { scaled << expanded }
|
|
end
|
|
scaled
|
|
end
|
|
|
|
fps = 24
|
|
scale = 1
|
|
band = 2
|
|
frames : Int32? = nil
|
|
no_color = false
|
|
use_alt = true
|
|
show_last = false
|
|
|
|
OptionParser.parse do |p|
|
|
p.banner = "Animated crystal"
|
|
p.on("--fps=FPS", "Frames per second") { |s| fps = s.to_i }
|
|
p.on("--scale=N", "Integer scale >=1") { |s| scale = s.to_i; scale = 1 if scale < 1; scale = 8 if scale > 8 }
|
|
p.on("--band=W", "Highlight band width") { |s| band = s.to_i; band = 1 if band < 1; band = 10 if band > 10 }
|
|
p.on("--frames=N", "Stop after N frames") { |s| frames = s.to_i }
|
|
p.on("--no-color", "Disable ANSI colors") { no_color = true }
|
|
p.on("--no-alt", "Do not use alternate screen") { use_alt = false }
|
|
p.on("--show-last", "Print last frame after exit") { show_last = true }
|
|
p.on("-h", "--help", "Show help") { puts p; exit 0 }
|
|
end
|
|
|
|
shape = scale_shape(BASE_SHAPE, scale)
|
|
height = shape.size
|
|
width = shape.max_of(&.size)
|
|
|
|
base_color = no_color ? nil : "\e[38;5;45m"
|
|
highlight_color = no_color ? nil : "\e[38;5;123m"
|
|
reset = no_color ? nil : "\e[0m"
|
|
|
|
running = true
|
|
Signal::INT.trap { running = false }
|
|
|
|
phase = 0
|
|
period = (width + height)
|
|
band_units = band * scale
|
|
|
|
if use_alt
|
|
STDOUT << String.build { |io| enter_alt(io) }
|
|
else
|
|
STDOUT << "\e[?25l"
|
|
end
|
|
STDOUT.flush
|
|
|
|
begin
|
|
frame_target_ns = (1_000_000_000_i64 // fps)
|
|
frames_left = frames
|
|
last_frame : String? = nil
|
|
while running
|
|
sz = terminal_size
|
|
top = ((sz.rows - height) // 2) + 1
|
|
left = ((sz.cols - width) // 2) + 1
|
|
top = 1 if top < 1
|
|
left = 1 if left < 1
|
|
|
|
start = Time.monotonic
|
|
|
|
frame_str = String.build do |io|
|
|
io << "\e[2J"
|
|
y = 0
|
|
while y < height
|
|
move_cursor(io, top + y, left)
|
|
row = shape[y]
|
|
x = 0
|
|
while x < row.size
|
|
ch = row.byte_at(x).unsafe_chr
|
|
if ch != ' '
|
|
sum = x + y
|
|
d = sum - phase
|
|
d %= period
|
|
d += period if d < 0
|
|
d = period - d if d > period // 2
|
|
if d <= band_units
|
|
io << highlight_color if highlight_color
|
|
else
|
|
io << base_color if base_color
|
|
end
|
|
io << '#'
|
|
else
|
|
io << ' '
|
|
end
|
|
x += 1
|
|
end
|
|
io << reset if reset
|
|
y += 1
|
|
end
|
|
io << reset if reset
|
|
end
|
|
last_frame = frame_str
|
|
STDOUT << frame_str
|
|
STDOUT.flush
|
|
|
|
phase += 1
|
|
phase = 0 if phase >= period
|
|
|
|
if frames_left
|
|
frames_left -= 1
|
|
break if frames_left <= 0
|
|
end
|
|
|
|
elapsed = Time.monotonic - start
|
|
sleep_ns = frame_target_ns - elapsed.nanoseconds
|
|
if sleep_ns > 0
|
|
sleep sleep_ns.nanoseconds
|
|
end
|
|
end
|
|
ensure
|
|
if use_alt
|
|
STDOUT << String.build { |io| leave_alt(io) }
|
|
else
|
|
STDOUT << "\e[0m\e[?25h"
|
|
end
|
|
STDOUT.flush
|
|
if show_last && last_frame
|
|
STDOUT << last_frame
|
|
STDOUT.flush
|
|
end
|
|
end
|
|
|
|
|