first commit
This commit is contained in:
commit
57452ef867
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
crystal.dwarf
|
||||
crystal
|
||||
.DS_Store
|
||||
186
crystal.cr
Normal file
186
crystal.cr
Normal file
@ -0,0 +1,186 @@
|
||||
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
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user