#!/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 and direction. a = Math::PI / 13 * (ARGV.size > 0 ? -1 : 1) # 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. z = [] r = u = b = y = 0 ARGF.each_line{|i| i.each_grapheme_cluster{|j| if (k = j.ord) < 33 r += k == 32 ? 1 : k == 9 ? 8 - r % 8 : k == 10 ? -r : 0 u += k == 10 ? 1 : 0 else z += [[u, r, j]] b += r y += u r += 1 end } } # Only process input if there are non-whitespace characters. if (s = z.size) > 0 # 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. b /= s y /= s # Apply transformations. m, n = z[0] i = Math::tan(a / 2) z.map!{|d| # Center contents. p = d[1] - b q = d[0] - y # Shear in X direction. # [1 -tan(A/2) * [p # 0 1 ] q] p -= (i * q).round # Shear in Y direction. # [1 0 * [p # sin(A) 1] q] q += (Math::sin(a) * p).round # Shear in X direction again. # [1 -tan(A/2) * [p # 0 1 ] q] p -= (i * q).round # Update upper left corner of bounding box. n = [n, p].min m = [m, q].min [q, p, d[2]] } # Reassemble output. r = n u = m z.sort.each{|d| p, q = d r = u < p ? n : r print "\n" * (p - u), " " * (q - r), d[2] u = p r = q + 1 } # Always end with newline. print "\n" end