#!/usr/bin/ruby require 'io/console' CHAR_HEIGHT = 2.0 LINE_SCALE = 1 / 6.0 EYE_POSITION_SCALE = 0.7 EYE_SCALE = 0.23 EYE_HORIZONTAL_SCALE = 2.5 MOUTH_POSITION_SCALE = 1.4 MOUTH_HEIGHT_SCALE = 0.5 MOUTH_WIDTH_SCALE = 3.7 MOUTH_BAR1_SCALE = 0.35 MOUTH_BAR2_SCALE = 0.7 # Distance between two points. def distance(ax, ay, bx, by) Math.hypot(ax - bx, ay - by) end # Approximate distance from point to ellipse. def ellipse_distance(x, y, cx, cy, rx, ry) angle = 0 min_distance = 1e99 step = Math::PI # Search for closest point on ellipse. 16.times{ min_angle = angle (-1..1).each{|d| a = angle + d * step px = rx * Math.cos(a) py = ry * Math.sin(a) d = distance(x - cx, y - cy, px, py) if min_distance > d min_distance = d min_angle = a end } angle = min_angle step /= 2.0 } return min_distance 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 line_distance(x, y, ax, ay, bx, by) dx = bx - ax dy = by - ay min_distance = 1e99 t0 = 0 t1 = 1 16.times{ d0 = distance(x, y, ax + dx * t0, ay + dy * t0) d1 = distance(x, y, ax + dx * t1, ay + dy * t1) if d0 < d1 t1 = (t0 + t1) / 2.0 else t0 = (t0 + t1) / 2.0 end min_distance = (d0 + d1) / 2.0 } return min_distance end # Return minimum distance to closest drawing edge. def shape_distance(x, y, r1, r2) ry = r1 * EYE_POSITION_SCALE rh1 = r1 * EYE_HORIZONTAL_SCALE my = -r1 * MOUTH_POSITION_SCALE mw = r1 * MOUTH_WIDTH_SCALE mh = r1 * MOUTH_HEIGHT_SCALE [line_distance(x, y, -mw, my, mw, my), line_distance(x, y, -mw * MOUTH_BAR2_SCALE, my - mh, -mw * MOUTH_BAR2_SCALE, my + mh), line_distance(x, y, -mw * MOUTH_BAR1_SCALE, my - mh, -mw * MOUTH_BAR1_SCALE, my + mh), line_distance(x, y, 0, my - mh, 0, my + mh), line_distance(x, y, mw * MOUTH_BAR1_SCALE, my - mh, mw * MOUTH_BAR1_SCALE, my + mh), line_distance(x, y, mw * MOUTH_BAR2_SCALE, my - mh, mw * MOUTH_BAR2_SCALE, my + mh), ellipse_distance(x, y, 0, ry, rh1, r1), ellipse_distance(x, y, 0, ry, r2, r2)].min end # Try fitting a square inside the terminal, and then set the vertical # diameter of outer eye ellipse proportional to that square. height, width = IO.console.winsize width -= 1 # Don't use rightmost column. s = [width, height * CHAR_HEIGHT].min r1 = s * EYE_SCALE # Scale line thickness proportional to radius. line_size = r1 * LINE_SCALE # Inner eye circle will be slightly smaller than outer eye ellipse. r2 = r1 - line_size * 2 # Check if mouth would fit horizontally, scale everything down if it doesn't. if (r1 * MOUTH_WIDTH_SCALE + line_size) * 2 > width scale = width / ((r1 * MOUTH_WIDTH_SCALE + line_size) * 2.0) r1 *= scale r2 *= scale line_size *= scale end height.times{|i| y = (height / 2.0 - i) * CHAR_HEIGHT width.times{|j| x = j - width / 2.0 if shape_distance(x, y, r1, r2) < line_size print "@" else print " " end } print "\n" }