#!/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 # Set rotation direction based on number of arguments. a = ARGV.size > 0 ? -A : A # 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 = [] cursor_x = cursor_y = cx = cy = 0 ARGF.each_line{|line| line.each_grapheme_cluster{|c| if c.ord < 33 cursor_x += c == " " ? 1 : c == "\t" ? 8 - cursor_x % 8 : c == "\n" ? -cursor_x : 0 cursor_y += c == "\n" ? 1 : 0 else data += [[cursor_y, cursor_x, c]] cx += cursor_x cy += cursor_y cursor_x += 1 end } } # Only process input if there are non-whitespace characters. if not data.empty? # Compute center of mass. # # We rotate by center of mass instead of by center of bounding box. # The latter would have been more straightforward to calculate, but # also tend to be more unstable depending on input shape. In # comparison, if we rotate by center of mass, we will often be able # to recover the original text by reversing the rotation direction. # # The down side is that the rotation center is now less predictable, # but it really only mattered if we are concerned about reversing # rotation. shift_x = cx / data.size shift_y = cy / data.size # 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 def output(x) = print x data.sort.each{|t| y, x = t if cursor_y < y output "\n" * (y - cursor_y) cursor_y = y cursor_x = min_x end output " " * (x - cursor_x) + t[2] cursor_x = x + 1 } # Always end with newline. output "\n" end