use Cairo; use Fornax::Hex2RGB; subset File of Str where *.IO.f; #| Parses fornax format file to extract metadata. grammar Metadata { rule TOP { } token rows { 'rows:' <(\d+)> } token cols { 'cols:' <(\d+)> } } proto MAIN(|) is export { unless so @*ARGS { put $*USAGE; exit }; {*} } #| Collection of tools to visualize Path Finding Algorithms multi sub MAIN( File $input, #= fornax format file (solved) IO() :$out = '/tmp', #= output directory (default: /tmp) Int() :$batch = 4, #= batch size (generate frames in parallel) Rat() :$frame-rate = 1, #= frame rate (default: 1) Bool :$skip-video, #= skip video solution Bool :$verbose = True, #= verbosity ) is export { my IO() $output = "%s/fornax-%s".sprintf( $out.absolute, ('a'...'z', 'A'...'Z', 0...9).roll(8).join ); mkdir $output; die "Output directory doesn't exist" unless $output.d; put "[fornax] Output: '$output'" if $verbose; my Str @lines = $input.IO.lines; my Int() %meta{Str} = Metadata.parse(@lines.first).Hash or die "Cannot parse metadata"; # Cells as defined by fornax format. constant $PATH = '.'; constant $BLOK = '#'; constant $DEST = '$'; constant $VIS = '-'; constant $CUR = '@'; constant %CANVAS = :1920width, :1080height; # Colors. constant %C = ( red => "#f2b0a2", blue => "#b5d0ff", cyan => "#c0efff", black => "#000000", white => "#ffffff", green => "#aecf90", pointer => "#093060", pointer-red => "#5d3026", pointer-green => "#184034", ).map: {.key => hex2rgb(.value)}; # Every cell must be square. Get the maximum width, height and use # that to decide which is to be used. my Int %cell = width => %CANVAS div %meta, height => %CANVAS div %meta; my Int $side; my Int %excess = :0width, :0height; # Consider width if cells with dimension (width * width) fit # within the canvas, otherwise consider the height. if (%cell * %meta) < %CANVAS { %excess = %CANVAS - (%cell * %meta); $side = %cell; } else { %excess = %CANVAS - (%cell * %meta); $side = %cell; } enum IterStatus ; my Promise @p; for @lines.skip.kv -> $idx, $iter is rw { # Wait until all scheduled jobs are finished, then empty the # array and continue. if @p.elems == $batch { await @p; @p = []; } push @p, start { my IterStatus $status; given $iter.substr(0, 1) { when '|' { $status = Completed } when '!' { $status = Blocked } default { $status = Walking } }; # Remove marker. $iter .= substr(1) if $status == Completed|Blocked; put "[fornax] $idx $iter $status" if $verbose; my @grid = $iter.comb.rotor: %meta; warn "Invalid grid: $idx $iter $status" unless @grid.elems == %meta; given Cairo::Image.create( Cairo::FORMAT_ARGB32, %CANVAS, %CANVAS ) { given Cairo::Context.new($_) { # Paint the entire canvas white. .rgb: |%C; .rectangle(0, 0, %CANVAS, %CANVAS); .fill; for ^%meta -> $r { for ^%meta -> $c { my Int @target = %excess div 2 + $c * $side, %excess div 2 + $r * $side, $side, $side; .rectangle: |@target; given @grid[$r][$c] -> $cell { when $cell eq $CUR { .rgba: |%C, 0.56; .rgba: |%C, 0.72 if $status == Completed; .rgba: |%C, 0.72 if $status == Blocked; } when $cell eq $VIS { .rgba: |%C, 0.72; .rgba: |%C, 0.96 if $status == Completed; .rgba: |%C, 0.96 if $status == Blocked; } when $cell eq $BLOK { .rgba: |%C, 0.56 } when $cell eq $DEST { .rgb: |%C } default { .rgba: |%C, 0.08 } } .fill :preserve; .rgb: |%C; .stroke; } } } .write_png("%s/%08d.png".sprintf: $output, $idx); .finish; } } } # Wait for remaining jobs to finish. await @p; put "[fornax] Generated images." if $verbose; unless $skip-video { put "[fornax] Creating a slideshow." if $verbose; my Str $log-level = $verbose ?? "info" !! "error"; run «ffmpeg -loglevel "$log-level" -r "$frame-rate" -i "$output/\%08d.png" -vf 'tpad=stop_mode=clone:stop_duration=4' -vcodec libx264 -crf 28 -pix_fmt yuv420p "$output/solution.mp4"»; } put "[fornax] Output: '$output'"; } multi sub MAIN( Bool :$version #= print version ) { say "Fornax v" ~ $?DISTRIBUTION.meta; }