#!/usr/bin/ruby -w # https://www.ocf.berkeley.edu/~fricke/projects/israel/paeth/rotation_by_shearing.html # 1. Shear in X by -tan(angle/2) # 2. Shear in Y by sin(angle) # 3. Shear in X by -tan(angle/2) # Rotation angle. A = Math::PI / 13 # Load input as a list of (y,x,grapheme) tuples. # # This is expensive in terms of memory use. A different way to do it would # be to maintain input as a list of lines, and adjust leading whitespaces to # do all the transforms, but it's easier to do the transformations and # spacing adjustments as separate passes. data = [] min_x = max_x = min_y = max_y = nil cursor_x = cursor_y = 0 ARGF.each_line{|line| line.each_grapheme_cluster{|c| case c when " " cursor_x += 1 when "\n" cursor_y += 1 cursor_x = 0 when "\t" cursor_x += 8 - cursor_x % 8 when "\0".."\x1f" # All other control characters are silently dropped. else data += [[cursor_y, cursor_x, c]] if min_x min_x = [min_x, cursor_x].min max_x = [max_x, cursor_x].max max_y = [max_y, cursor_y].max # Don't need to update min_y since we can't go backwards. else min_x = max_x = cursor_x min_y = max_y = cursor_y end cursor_x += 1 end } } # Only process input if there are non-whitespace characters. if min_x # Center contents. shift_x = max_x + min_x >> 1 shift_y = max_y + min_y >> 1 # Apply transformations. min_y, min_x = data[0] rx = Math::tan(A / 2) ry = Math::sin(A) data.map!{|t| # Center contents. x = t[1] - shift_x y = t[0] - shift_y # Shear in X direction. # [1 -tan(A/2) * [x # 0 1 ] y] x -= (rx * y).round # Shear in Y direction. # [1 0 * [x # sin(A) 1] y] y += (ry * x).round # Shear in X direction again. # [1 -tan(A/2) * [x # 0 1 ] y] x -= (rx * y).round # Update upper left corner of bounding box. min_x = [min_x, x].min min_y = [min_y, y].min [y, x, t[2]] } # Reassemble output. cursor_x = min_x cursor_y = min_y data.sort.each{|t| y, x = t if cursor_y < y print "\n" * (y - cursor_y) cursor_y = y cursor_x = min_x end print " " * (x - cursor_x), t[2] cursor_x = x + 1 } # Always end with newline. print "\n" end