diff options
author | Andinus <andinus@nand.sh> | 2022-01-12 12:42:38 +0530 |
---|---|---|
committer | Andinus <andinus@nand.sh> | 2022-01-12 12:42:38 +0530 |
commit | 25f65f212c210638d15d73c0c8dfe1fc08bd96a2 (patch) | |
tree | 48a98fcd1b0440465eca8e3d8684a306605c3b28 | |
parent | 59d0cd4fbb98cffa28120714b87008573777f429 (diff) | |
download | octans-25f65f212c210638d15d73c0c8dfe1fc08bd96a2.tar.gz |
Add visualize feature using Cairo
This takes parts from Fornax: https://github.com/andinus/fornax Each step is visualized and a video solution is generated.
-rw-r--r-- | lib/Octans/CLI.rakumod | 34 | ||||
-rw-r--r-- | lib/Octans/GenerateFrame.rakumod | 63 | ||||
-rw-r--r-- | lib/Octans/Hex2RGB.rakumod | 10 | ||||
-rw-r--r-- | lib/Octans/WordSearch.rakumod | 17 |
4 files changed, 118 insertions, 6 deletions
diff --git a/lib/Octans/CLI.rakumod b/lib/Octans/CLI.rakumod index f3501df..4d94b69 100644 --- a/lib/Octans/CLI.rakumod +++ b/lib/Octans/CLI.rakumod @@ -1,6 +1,7 @@ use Octans::Puzzle; use Octans::WordSearch; use Octans::Puzzle::Get; +use Octans::GenerateFrame; proto MAIN(|) is export { unless so @*ARGS { say $*USAGE; exit }; {*} } @@ -9,6 +10,7 @@ multi sub MAIN( Str :$dict = (%?RESOURCES<mwords/354984si.ngl> // "/usr/share/dict/words").Str, #= dictionary file Int :$length = 7, #= minimum word length (default: 7) + Bool :$visualize, #= produces a video output Bool :$verbose, #= increase verbosity ) is export { # @dict holds the sorted dictionary. Only consider words >= 7 @@ -31,18 +33,40 @@ multi sub MAIN( " $_".say for $puzzle.grids; } + constant $SIDE = 128; + my Int %meta = :rows($puzzle.grids.elems), :cols($puzzle.grids[0].elems); + my Int %canvas = :width(%meta<cols> * $SIDE), :height(%meta<rows> * $SIDE); + + my IO() $output = "%s/octans-%s".sprintf( + '/tmp', ('a'...'z', 'A'...'Z', 0...9).roll(8).join + ); + if $visualize { + mkdir $output; + put "Output: $output"; + die "Output directory doesn't exist" unless $output.d; + } + + my Int $iter = 0; # start-pos block loops over each starting position. start-pos: for $puzzle.gray-squares -> $pos { # gather all the words that word-search finds starting from # $pos. word: for gather word-search( - @dict, $puzzle.grids, $pos[0], $pos[1], + @dict, $puzzle.grids, $pos[0], $pos[1], :visualize ) -> ( # word-search returns the word along with @visited which # holds the list of all grids that were visited when the # word was found. $word, @visited ) { + if $visualize { + generate-frame( + :%canvas, :side($SIDE), :puzzle($puzzle.grids), :@visited, :%meta, + :out("%s/%08d.png".sprintf: $output, $iter++), :found($word ne "") + ); + next word; + } + printf "%s$word\n", $verbose ?? "\n" !! ""; next word unless so $verbose; @@ -61,6 +85,14 @@ multi sub MAIN( } } } + + if $visualize { + put "Creating a slideshow."; + my Str $log-level = $verbose ?? "info" !! "error"; + run «ffmpeg -loglevel "$log-level" -r 1.5 -i "$output/\%08d.png" + -vf 'tpad=stop_mode=clone:stop_duration=1' + -vcodec libx264 -crf 24 -pix_fmt yuv420p "$output/solution.mp4"»; + } } multi sub MAIN( diff --git a/lib/Octans/GenerateFrame.rakumod b/lib/Octans/GenerateFrame.rakumod new file mode 100644 index 0000000..ba7fc5b --- /dev/null +++ b/lib/Octans/GenerateFrame.rakumod @@ -0,0 +1,63 @@ +# This module has been adapted from Fornax::GenerateFrame. + +use Cairo; +use Octans::Hex2RGB; + +# Colors. +constant %C = ( + bg-main => "#ffffff", + + red-subtle-bg => "#f2b0a2", + blue-subtle-bg => "#b5d0ff", + cyan-subtle-bg => "#c0efff", + green-subtle-bg => "#aecf90", + + fg-main => "#000000", + + fg-special-cold => "#093060", + fg-special-warm => "#5d3026", + fg-special-mild => "#184034", + fg-special-calm => "#61284f", +).map: {.key => hex2rgb(.value)}; + +enum IterStatus <Walking Blocked Completed>; + +sub generate-frame( + :%canvas, :$out, :$side, :@puzzle, :@visited, :%meta, :$found +) is export { + given Cairo::Image.create( + Cairo::FORMAT_ARGB32, %canvas<width>, %canvas<height> + ) { + given Cairo::Context.new($_) { + # Paint the entire canvas white. + .rgb: |%C<bg-main>; + .rectangle(0, 0, %canvas<width>, %canvas<height>); + .fill; + + # This seems to be slower than creating an intermediate + # variable and assigning from that. Difference is not much + # so we'll ignore it. + for ^%meta<rows> X ^%meta<cols> -> ($r, $c) { + my Int @target = $c * $side, $r * $side, + $side, $side; + .rectangle: |@target; + + if @visited[$r][$c] { + .rgba: |%C<cyan-subtle-bg>, 0.72; + .rgba: |%C<green-subtle-bg>, 0.84 if $found; + .fill :preserve; + } + + .select_font_face("Mono", Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_NORMAL); + .set_font_size(72.0); + .move_to($c * $side + 32, ($r + 1) * $side - 28); + + .rgb: |%C<fg-main>; + .show_text: @puzzle[$r][$c].uc; + .stroke; + } + } + .write_png($out); + .finish; + } +} diff --git a/lib/Octans/Hex2RGB.rakumod b/lib/Octans/Hex2RGB.rakumod new file mode 100644 index 0000000..72cb848 --- /dev/null +++ b/lib/Octans/Hex2RGB.rakumod @@ -0,0 +1,10 @@ +# This module has been adapted from Fornax::Hex2RGB. + +#| Takes hex value and returns RGB equivalent. +sub hex2rgb(Str $hex --> List) is export { + # Skip the first character, group each by 2 and parse as base 16. + # Divide by 255 to return value between 0, 1. + $hex.comb.skip.rotor(2).map( + *.join.parse-base(16) / 255 + )>>.Rat +} diff --git a/lib/Octans/WordSearch.rakumod b/lib/Octans/WordSearch.rakumod index 23487dc..73d05cd 100644 --- a/lib/Octans/WordSearch.rakumod +++ b/lib/Octans/WordSearch.rakumod @@ -1,9 +1,9 @@ use Octans::Neighbors; use Octans::RangeSearch; -# word-search walks the given grid & tries to find words in the -# dictionary. It walks in Depth-First manner (lookup Depth-First -# search). +#| word-search walks the given grid & tries to find words in the +#| dictionary. It walks in Depth-First manner (lookup Depth-First +#| search). sub word-search( # @dict holds the dictionary. @puzzle holds the puzzle. @dict, @puzzle, @@ -18,12 +18,19 @@ sub word-search( Int $y, Int $x, $str? = @puzzle[$y][$x], # @visited holds the positions that we've already visited. - @visited? is copy --> List + @visited? is copy, + + # If true then every iteration of walking is returned. + Bool :$visualize, + + --> List ) is export { # If @visited was not passed then mark the given cell as visited # because it's the cell we're starting at. @visited[$y][$x] = True unless @visited; + take "", @visited if $visualize; + # neighbor block loops over the neighbors of $y, $x. neighbor: for neighbors(@puzzle, $y, $x).List -> $pos { # Move on to next neighbor if we've already visited this one. @@ -53,7 +60,7 @@ sub word-search( # subset of words that start with "a", so keeping # this in mind we pass the output of last # range-starts-with (@list). - @list, @puzzle, $pos[0], $pos[1], $word, @visited + @list, @puzzle, $pos[0], $pos[1], $word, @visited, :$visualize, ); } } |