#!/usr/bin/ruby require 'io/console' # {{{ Eye YO = 2.0 T = 1 / 6.0 S = 0.7 U = 0.23 Y = 2.5 A = 1.4 M = 0.5 I = 3.7 K = 0.35 O = 0.7 # Distance between two points. def D(x, y, p, q) Math.hypot(x - p, y - q) end # Approximate distance from point to ellipse. def E(x, y, w, i, j) s = 0 n = 1e99 m = Math::PI # Search for closest point on ellipse. 16.times{ t = s (-1..1).each{|d| a = s + d * m p = i * Math.cos(a) q = j * Math.sin(a) d = D(x, y - w, p, q) if n > d n = d t = a end } s = t m /= 2.0 } n end # Approximate distance from point to line segment. # # There is a more analytical way to do this, but it's simpler to handle # the edge cases with a straightforward binary search. This costs a # bit of precision and time, but we are mostly optimizing for code size. def L(x, y, i, j, p, q) p -= i q -= j n = 1e99 s = 0 t = 1 16.times{ d = D(x, y, i + p * s, j + q * s) b = D(x, y, i + p * t, j + q * t) m = (s + t) / 2.0 d < b ? (t = m) : (s = m) n = (d + b) / 2.0 } n end # }}} # {{{ Fill # Flip points across the line Y=X. # # B C D C # A D -> A B # # f g J K p m L K # e h I L o n I J # D C n m -> B C h g # A B o p A D e f def F(o) o.map{|x, y| [y, x]} end # Flip points across the line Y=-X. # # B C B A # A D -> C D # # f g J K f e D A # e h I L g h C B # D C n m -> J I n o # A B o p K L m p def G(o) o.map{|x, y| [-y, -x]} end # Shift points by some offset. def V(o, p, q) o.map{|x, y| [x + p, y + q]} end # Add list of points to table, preserving existing table entries. def N(t, w, h, o) (t[w] ||= {})[h] ||= o end # Generate rectangles of the same height as a particular unit rectangle. def Z(t, m, p, q) w = p while w <= m * 2 N(t, w + p, q, t[w][q] + V(t[p][q], w, 0)) w += p end end # Generate table of space filling curves. # # Returns a table indexed by [width][height] with list of points as values. # All point list start in lower left corner and end in lower right corner. def W(m, n) # Seed table with all rectangles of height 1. t = {} (1 .. m * 2).each{|w| N(t, w, 1, (0 .. w - 1).to_a.map{|x| [x, 0]}) } # Expand table. s = 1 while s <= n # Generate all rectangles from size (s * 2, s) up to (width, s * 2). (1 .. s).each{|h| # Generate rectangle of size (s * 2, s + h), using the # following pattern: # +-----------+ # | R | # +-----+-----+ # | | | # | S' | S" | # | | | # +-----+-----+ # * S' is a square of size "s" starting in lower left corner # and ending in upper left corner. # # * R is a rectangle starting in lower left corner and ending # in lower right corner. # # * S" is a square starting in upper right corner and ending # in lower right corner. # # When height of R is "s", this essentially generates a # Hilbert curve of size 2*s. N(t, s * 2, s + h, F(t[s][s]) + V(t[s * 2][h], 0, s) + V(G(t[s][s]), s * 2 - 1, s - 1)) # Copy rectangles to the right. Z(t, m, s * 2, s + h) } s *= 2 end # Generate additional rectangles to fill smaller gaps. s = n while s > 1 p = n - s while p > 0 q = s + p while q > 0 w = s + q h = s + p if !t[w][h] && t[s][s] && t[s + q][p] && t[s][q] # Generate rectangles using the following pattern: # +--------+ # | P | # +-----+--+ # | | | # | S' |Q | # | | | # +-----+--+ # * S' is a square of size "s" starting in lower left # corner and ending in upper left corner. # # * P is a rectangle of width "s+q" and height "p" starting # lower left corner and ending in lower right corner. # # * Q is a rectangle of width "q" and height "s" starting # upper right corner and ending in lower right corner. N(t, w, h, F(t[s][s]) + V(t[w][p], 0, s) + V(G(t[s][q]), w - 1, s - 1)) # Copy rectangles to the right. Z(t, m, w, h) end q -= 1 end p -= 1 end s -= 1 end t end # Given a generated table of point lists, recursively fill rectangle. # # This function is needed because generate_table isn't guaranteed to # cover the expected size. This function fills those gaps by relaxing # the constraint that paths have to end in lower right corner. def R(t, m, n) # Degenerate cases from recursive calls. if m * n == 0 return [] end # Try dividing rectangle into left and right halves. # +-------+---+ # | L | R | # +-------+---+ w = m while w > 0 if t[w] && t[w][n] return t[w][n] + V(F(R(t, n, m - w)), w, 0) end w -= 1 end # Don't have any rectangle of the right height, try shaving off the # bottom edge. t[m][1] + R(t, n - 1, m).map{|x, y| [m - 1 - y, 1 + x]} end # Build curve that fills a particular rectangular area. def C(m, n) m < n ? F(C(n, m)) : R(W(m, n), m, n) end # }}} J = Time.now h, w = IO.console.winsize o = C(w / 2, h / 2).map{|x, y| [2 * x, 2 * y]} w = o.map{|x, y| x}.max h = o.map{|x, y| y}.max # Only draw if there are enough points to draw. if w && w > 1 && h > 1 # Compute eye parameters. m = [w, h * YO].min * U # Scale line thickness proportional to radius. l = m * T # Inner eye circle will be slightly smaller than outer eye ellipse. n = m - l * 2 # Check if mouth would fit horizontally, scale everything down if it doesn't. if (m * I + l) * 2 > w s = w / ((m * I + l) * 2.0) m *= s n *= s l *= s end # Allocate array to remember which cells had curve points. This # will be used to make sure that we don't overwrite existing curve # points with empty space. a = [] (h + 2).times{ a += [Array.new(w + 2)] } # Miscellaneous setup steps. print "\n" * h P = ["\e[1D", "", "\e[1C"] Q = ["\e[1B", "", "\e[1A"] d, b = o[0] c = 0 # Previous painted pixel, set to non-nil after first iteration. r = nil # Try to satisfy two timing constraints: # - Program takes same amount of total time regardless of terminal size. # - Drawing routine tries to maintain constant frame rate at each frame. # # 30.0 below is the target total time (seconds), for which we # subtract the time taken to generate the curve itself to get the # total time allocated for drawing. # # The theme is perfect fit: we want to fill the terminal perfectly # and we want to take exactly the same predetermined amount of time # to do so. This works reasonably well for terminal sizes up to # 132x40 or so, any larger and all the distance computation time would # dominate, and we might end up taking a minute or so. # # Calculate frame_delay based on number of points. Due to scale2x, # the number of frames will be almost exactly twice the number of # points, hence the 2.0 below. B = (30.0 - (Time.now - J)) / (o.length * 2.0) o.each{|x, y| # Move cursor toward current point. # # (0, 0) is bottom left corner. while !r || d != x || b != y s = d < x ? 1 : d > x ? -1 : 0 t = b < y ? 1 : b > y ? -1 : 0 d += s b += t print P[s + 1], Q[t + 1] # Draw a group of pixels. (-1 .. 1).each{|v| q = b + v t = (q - h / 2.0) * YO (-1 .. 1).each{|u| p = d + u if p >= 0 && p <= w && q >= 0 && q <= h && !a[q][p] if u == 0 && v == 0 r = ["M", "W"][(p + q) % 2] a[q][p] = r else r = " " end s = p - w / 2.0 # Compute distance to shape edges. e = m * S f = m * Y i = -m * A j = m * I k = m * M # Bright white on yellow background near shape edges, # otherwise use gray on black background. # # Yellow is chosen to match Miko's eye color. Actual # color depends on terminal palette and might be anything # from bright yellow to dark brown to something else # entirely. print Q[1 + v], P[1 + u], [L(s, t, -j, i, j, i), L(s, t, -j * O, i - k, -j * O, i + k), L(s, t, -j * K, i - k, -j * K, i + k), L(s, t, 0, i - k, 0, i + k), L(s, t, j * K, i - k, j * K, i + k), L(s, t, j * O, i - k, j * O, i + k), E(s, t, e, f, m), E(s, t, e, n, n)].min < l ? "\e[97;43m" : "\e[90;40m", r, "\e[1D\e[0m", P[1 - u], Q[1 - v] end } } STDOUT.flush # Optionally pause after each frame to maintain desired total runtime. # # The goal is to maintain total runtime, hence how delay is based on # total elapsed time so far. A different alternative is to try to # maintain a constant frame rate, but that is more difficult because # sleep isn't that precise. c += 1 sleep [B * c - (Time.now - J), 0].max end } # Return cursor to start of line, plus extra adjustment to move # cursor down if we ended up somewhere other than the bottom line. print "\n" * (o[-1][1] + 1) end