Visualize Visual Eyes

You may or may not remember Xeyes, a demo program that came with (and, as far as I know, still comes with) the X window system. Its purpose was to draw a pair of eyes that followed your mouse cursor:

Your challenge is to recreate Xeyes with ASCII art. Write a program or function that draws two ASCII art eyes (specified below) wherever the user clicks and then moves their pupils to point in the direction of the cursor.

The above GIF is a recording of this non-golfed Ruby implementation, which can be run with any recent version of Ruby. You may also find it useful as reference for Xterm control sequences.

Specifications

This is , so the solution with the fewest bytes wins.

This is an challenge, so your program must draw using ASCII characters—specifically, the characters -, ., |, ', 0, space, and newline.1 2

This is an challenge, so your program must accept input and draw its output in realtime.3

Before your program begins accepting input, it should initialize a blank canvas of at least 20 rows and 20 columns. It should not draw anything until the user clicks on the canvas.

Any time the user clicks4 on the canvas, the program should clear any previous output and then draw these ASCII eyes on the canvas, centered on the character nearest the location of the mouse cursor.5 6 (Below, ✧ represents the mouse cursor and should not be drawn.)

.---. .---.
|   | |   |
|  0|✧|0  |
|   | |   |
'---' '---'


Note how the pupils "point" toward the cursor.

Any time the mouse cursor moves on the canvas, the program should re-draw the pupils so that they continue pointing toward the cursor,7 e.g.:

             ✧

.---. .---.
|  0| |  0|
|   | |   |
|   | |   |
'---' '---'


Pupil pointing

Suppose we enumerated the positions of the inner nine characters of each eye like so:

.---.
|678|
|591|
|432|
'---'


The pupil will be drawn at one of the locations 1-9. To decide which one, pretend the characters are square and that the canvas is a Cartesian grid, with the center of the 9 character at (0, 0), the center of 1 at (1, 0), and so on. When the program receives input—a click or movement—it should map the input location to the nearest grid coordinate 𝑀. If 𝑀 is (0, 0), the pupil should be drawn at (0, 0), i.e. the location of the 9 above. Otherwise, it should be drawn as described below.

Imagine a Cartesian plane superimposed on the grid and divided into octants numbered 18:

If 𝑀 lies within octant 1, then the pupil should be drawn at the location of 1 above, i.e. at (1, 0). If 𝑀 is in octant 2 it should be drawn at 2—and so on. To illustrate, the below image shows part of the grid color-coded according to where the pupil should be drawn when the mouse cursor is at a particular location. When, for example, the cursor is at any of the green coordinates (keeping in mind that the grid coordinates lie at the squares' centers, not their corners), the pupil should be drawn at 4.

The two eyes' pupils move independently, so for each eye repeat the process with 𝑀 relative to that eye's center.

Notes

1. This isn't a challenge. The output must be a grid of characters. You may, of course, use graphics routines to draw a grid of characters.

2. Whitespace may be drawn (or, rather, not drawn) however is convenient. An empty spot in the grid looks the same as a space character and will be considered equivalent.

3. "Real-time" is defined here as fewer than 200ms between input and the corresponding output being drawn.

4. It is at your discretion which mouse button(s) are observed for input, and whether a press or release constitutes a "click."

5. The canvas must be cleared, or the visual equivalent must be achieved. With a terminal-based solution, for example, printing a new canvas below the previous canvas is not considered equivalent.

6. When the user clicks near the edge of the canvas such that some of the eye characters would be drawn beyond its edge, the behavior is undefined. However, the program must continue running normally upon subsequent clicks.

7. When the mouse cursor leaves the "canvas," the behavior is undefined, but the program must continue running normally when the cursor re-enters the canvas.

8. A text cursor may appear on the canvas, as long as it does not obscure the output.

Standard loopholes are forbidden.

• @Οurous Since how many minutes is "a few" in that case would depend on how much memory the system has, and that could lead us into "this solution assumes the environment has 512GB of RAM" territory, I'm going to say that it must potentially run indefinitely. – Jordan Jan 16 '18 at 20:24
• @TaylorScott Nope. See note #6 (unless I’ve misunderstood your question). – Jordan Jan 17 '18 at 1:06
• @Οurous Yes, and no. If your target environment is usually one in which the default font is monospace (say, a terminal emulator, or code editor), then that’s fine. If using a monospace font in that environment usually takes extra configuration (as in a browser-based JS solution), that configuration must be part of your byte count (e.g. <pre> or font-family:monospace). – Jordan Jan 17 '18 at 2:03
• +1 for great title (or bad title, depending on how you take it) – FantaC Jan 17 '18 at 2:06
• @Οurous Nope, as long as it doesn’t terminate unexpectedly. – Jordan Jan 17 '18 at 13:53

Excel VBA 32-Bit, 630 590 Bytes

Declared worksheet subroutine that runs on mouse click which takes no input and produces a pair of eyes which follow the cursor. This is dependent upon the included helper function and type declaration, which must be placed in a normal module.

This version is calibrated to run at the default zoom of 100%. Breaks if you try to scroll or zoom.

Sub Worksheet_SelectionChange(ByVal t As Range)
With Cells
.Clear
.Font.Name="Courier" '<-- " added for highlighting only
.ColumnWidth=1.3
End With
Dim l As p,p As p
Q l
On Error Resume Next
Do
DoEvents
Q p
For i=0To 1
x=l.x+(-1)^i*56
n=Evaluate("=-Int(-8/Pi()*ATan2("&x-p.x &","&l.y-p.y+0.1 &"))")
n=Asc(-Int(-IIf(Abs(p.x-x)<7And Abs(p.y-l.y)<10,9,IIf(n<-6,8,n)-1)/2)+4)
j=1
For r=-2To 2
For c=0To 4
m=Mid(".---.|567||498||321|'---'",j,1)
t.Offset(r,c+IIf(i,-5,1))=Mid("0 '"&m,3+(m Like"[0-9]")+(Asc(m)=n))
j=j+1
Next c,r,i
Loop
End Sub


Helper Function and Type Declaration

Declare Sub Q Lib"user32"Alias"GetCursorPos"(l As p)
Type p
x As Long
y As Long
End Type


Ungolfed and Commented

This version is calibrated to be run at a zoom level of 400%.

''  must be placed in a worksheet code module

''  define this module to run whenever the user either clicks
''  or moves the selection with the arrow keys
Private Sub Worksheet_SelectionChange(ByVal T As Range)

''  Declare vars
Dim refPos  As POSITION, _
curPos  As POSITION, _
c       As Range, _
d       As String, _
i       As Integer, _
j       As Integer, _
n       As Integer, _
x       As Integer

''  Explicitly state that this works only on the
''  Worksheet for which this code has been defined
With Application.ActiveSheet

''  Clear eyes and escape var
Call .Cells.ClearContents

''  Define escape var
Let .[A1] = " "

''  Define reference position
Call GetCursorPos(refPos)

''  While not escaped
Do While [A1] = " "

''  Prevent Excel from appearing to freeze
Call VBA.DoEvents

''  Check where the cursor is
Call GetCursorPos(curPos)

''  Iterate over the eyes' indexes
For i = 0 To 1 Step 1

''  Define the reference center of the eye, left first
Let x = refPos.x + IIf(i, -168, 168)

'' figure out which of the directions to point the eye and assign that value to n
Let n = Evaluate("=-Int(-8/Pi()*ATan2(" & x - curPos.x & "," & refPos.y - curPos.y + 0.1 & "))")
Let n = Asc(-Int(-IIf(Abs(curPos.x - x) < 28 And Abs(curPos.y - refPos.y) < 40, 9, IIf(n < -6, 8, n) - 1) / 2) + 4)

''  define character index
Let j = 1

''  Iterate over the range in which the eye is to be drawn
For Each c In T.Offset(-2, IIf(i, -5, 1)).Resize(5, 5)

''  get correct char from the reference data
Let d = Mid(".---.|567||498||321|'---'", j, 1)

''  check if the char is a number, if so only keep it if it matches n
Let c.Value = IIf(d Like "[0-9]", IIf(Asc(d) = n, 0, " "), "'" & d)

'' iterate j
j = j + 1
Next c, i
Loop
End With
End Sub


Helper Function and Type Declaration

''  Declare the 64-Bit Window API function
Declare PtrSafe Function GetCursorPos Lib "user32" (ByRef posObj As POSITION) As LongLong

''  Define the POSITION type; 0,0 is top left of screen
Type POSITION
x As Long
y As Long
End Type

''  Pre-Operations for optimization
Sub Initialize()
With Cells

''  Define the font as being mono-spaced
.Font.Name = "Lucida Console"

''  Define the size of the cells to be tightly bound around a single char
.ColumnWidth = 1.5
.RowHeight = 15
End With
End Sub


Output

Gif

Higher Res Image

• This doesn’t match the specification in a few ways. 1. “Grid of characters” means single characters with distinct positions. When the mouse cursor is on, say, the rightmost ' character the output will be different from when it’s on the leftmost ' character. 2. The eyes’ position is not fixed. A mouse click should cause them to move to the clicked position. I’m flexible on the method of input (I’d accept, say, a virtual mouse cursor controlled by the arrow keys), but there are two distinct input events with distinct behavior: mouse movement and mouse click. – Jordan Jan 16 '18 at 23:11
• @Jordan I am not quite sure what you mean by point 1, could you please elaborate? As for point 2, the eyes are not static, and clicking on any cell on the sheet in which the subroutine is placed shall trigger the Worksheet_SelectionChange event and pass the calling range (Target or T in this case) - which redraws the eyes and a * in the calling cell – Taylor Scott Jan 17 '18 at 0:36
• Wrt #1, perhaps I’ve misunderstood from your screenshots, but it looks like each cell in your worksheet is a distinct position, i.e. “character” per the specification, but the eyes are drawn in a single cell of the worksheet. In the specification, though, the eyes are 11 columns and 5 rows, and the mouse cursor can be at any of the 55 positions within the eyes (or, of course, any position outside them). (cont.) – Jordan Jan 17 '18 at 0:48
• Furthermore, per the specification the pupils must move independently. When the user first clicks, for example, the cursor will be directly between the eyes, so the left pupil will be (referring to the 1–9 diagram under “Pupil pointing”) at position 1 and the right at position 5. If, then, the mouse cursor moves three characters to the right, the left pupil will still be at 1, but the right pupil will have moved to 9. You can see this independent-pupil behavior in the GIF at the top of the post. – Jordan Jan 17 '18 at 0:51
• Maybe change the two screenshots to a screen-to-gif? – Kevin Cruijssen Jan 17 '18 at 8:45

HTML + CSS + JavaScript (ES6), 93 + 19 + 278 276 = 388 bytes

w=7.8125
h=15
with(Math)r=round,
(onclick=e=>F.style=margin:-3.5em -6.5ch;left:${x=r(e.x/w)*w}px;top:${y=r(e.y/h)*h}px)({y:-40}),onmousemove=e=>(s=($,o)=>$.style=left:${a=atan2(Y=r((e.y-y)/h),X=r((e.x-x)/w+o)),X|Y?w*r(cos(a)):0}px;top:${X|Y?h*r(sin(a)):0}px)(L,3)&&s(R,-3)
*{position:relative
<pre id=F>.---. .---.
|   | |   |
| <a id=L>0</a> | | <a id=R>0</a> |
|   | |   |
'---' '---'

• Both X||Y can be golfed to X|Y to save 2 bytes. – Kevin Cruijssen Jan 17 '18 at 7:49
• Doesn't work so well when you click near the bottom of the container and have to scroll down. i.stack.imgur.com/s44KU.png Not sure if specific to the snippet wrapper, but was worth mentioning. – Draco18s no longer trusts SE Jan 17 '18 at 20:21
• @Οurous It's rather ambiguously worded: "centered at the location of the mouse cursor." Does "location" mean "grid cell" or can it mean "pixel"? I agree that the intent was probably the former, but the wording certainly seems to allow the latter. – DLosc Jan 17 '18 at 21:53
• @KevinCruijssen Unfortunately, that doesn't work – | ends up taking precedence over the ternary expression. – darrylyeo Jan 17 '18 at 23:24
• @darrylyeo No it doesn't? :S This JavaScript operator precedence table shows | and || on somewhat the same level, and both above ?:.. Both X||Y?w*r(cos(a)):0 and X||Y?h*r(sin(a)):0 are currently in the form boolean_condition?A:B. So when you change X||Y to X|Y it will do a bit-wise OR and then interpret as a boolean condition again. ((X||Y)?A:B vs (X|Y)?A:B, not X|(Y?A:B)). Also, I don't see any difference when I use "Copy snippet to answer" and change the || to |. Everything still works exactly the same as far as I can tell.. – Kevin Cruijssen Jan 18 '18 at 8:01

QBasic (QB64), 361 305 bytes

DO
WHILE _MOUSEINPUT
x=CINT(_MOUSEX)
y=CINT(_MOUSEY)
IF _MOUSEBUTTON(1)THEN l=x-3:k=y
IF(2<l)*(73>l)*(2<k)*(22>k)THEN CLS:FOR i=0TO 1:h=l+6*i:LOCATE k-2,h-2:?".---.":FOR j=1TO 3:LOCATE,h-2:?"|   |":NEXT:LOCATE,h-2:?"'---'":d=x-h:e=y-k:m=ABS(e/d):LOCATE k-SGN(e)*(m>=.5),h-SGN(d)*(m<=2):?"0":NEXT
WEND
LOOP


Left-click places the eyes. If placement of eyes would result in part of the eyes being out of bounds, the program "freezes" until a valid placement is made.

The main tricky part is placing the pupils. Most of the time, the coordinates of the pupil are just the center of the eye plus (sign(Δx), sign(Δy)), except that in octants 1 and 5, the y-coordinate equals y-center, and in octants 3 and 7, the x-coordinate equals x-center. Octant boundaries can be calculated using the slope m of the line from the center of the eye to the mouse coordinates. Conveniently, dividing by zero when calculating the slope gives floating-point infinity (+/-) rather than an error.

Ungolfed

' Loop forever
DO
' Do stuff if there is new mouse data (movement or click)
IF _MOUSEINPUT THEN
' Store the mouse coords rounded to the nearest integer
mouse_x = CINT(_MOUSEX)
mouse_y = CINT(_MOUSEY)
' If left mouse button was clicked, change location of eyes
IF _MOUSEBUTTON(1) THEN
' Store center coordinates of left eye
left_center_x = mouse_x - 3
center_y = mouse_y
END IF
' If eye location is in bounds, print the eyes and pupils
x_in_bounds = left_center_x > 2 AND left_center_x < 73
y_in_bounds = center_y > 2 AND center_y < 22
IF x_in_bounds AND y_in_bounds THEN
CLS
FOR eye = 1 TO 2
' eye = 1 for left eye, eye = 2 for right eye
IF eye = 1 THEN center_x = left_center_x
IF eye = 2 THEN center_x = left_center_x + 6
' Print eye borders
LOCATE center_y - 2, center_x - 2
PRINT ".---."
FOR row = 1 TO 3
LOCATE , center_x - 2
PRINT "|   |"
NEXT row
LOCATE , center_x - 2
PRINT "'---'"
' Calculate coordinates of pupil
xdiff = mouse_x - center_x
ydiff = mouse_y - center_y
slope = ydiff / xdiff
' For most cases, adding the sign of the diff to the center
' coordinate is sufficient
pupil_x = center_x + SGN(xdiff)
pupil_y = center_y + SGN(ydiff)
' But in octants 3 and 7, the x-coordinate is centered
IF ABS(slope) > 2 THEN pupil_x = center_x
' And in octants 1 and 5, the y-coordinate is centered
IF ABS(slope) < 0.5 THEN pupil_y = center_y
LOCATE pupil_y, pupil_x
PRINT "0"
NEXT eye
END IF   ' in bounds
END IF   ' mouse data
LOOP   ' forever

• It's been a decade or two since I used QB, but couldn't you use ?0 instead of ?"0"? This suggests you can use a numeric expression as well as strings. – Joey Jan 17 '18 at 15:47
• @Joey Hmm. Printing it as a number also prints a space before and after it... but come to think of it, I bet I could print the pupils first and then that wouldn't be a problem. Except then I'd have to print the left and right borders separately instead of as "| |". So it probably wouldn't save anything. "0" is only 2 bytes longer. – DLosc Jan 17 '18 at 19:33

6502 machine code (C64 + 1351 mouse), 630 bytes

00 C0 20 44 E5 A9 FF 85 5E A2 3F A9 00 8D 10 D0 8D 1B D0 9D C0 02 CA 10 FA A0
0A A2 1E B9 5A C2 9D C0 02 CA CA CA 88 10 F4 A9 0B 8D F8 07 A9 18 8D 00 D0 A9
32 8D 01 D0 A9 0D 8D 27 D0 A9 01 8D 15 D0 78 A9 60 8D 14 03 A9 C1 8D 15 03 58
D0 FE 84 FD 85 FE A8 38 E5 FD 29 7F C9 40 B0 04 4A F0 0A 60 09 C0 C9 FF F0 03
38 6A 60 A9 00 60 20 44 E5 A5 69 38 E9 05 B0 02 A9 00 C9 1E 90 02 A9 1D 85 FD
18 69 02 85 5C 69 06 85 5D A5 6A 38 E9 02 B0 02 A9 00 C9 15 90 02 A9 14 85 FE
18 69 02 85 5E A9 65 8D BB C0 A9 C2 8D BC C0 A9 04 85 02 A6 FE 20 F0 E9 A9 02
85 5F A4 FD A2 00 BD FF FF 91 D1 C8 E8 E0 05 D0 F5 C8 C6 5F D0 EE E6 FE A9 6A
8D BB C0 A9 C2 8D BC C0 C6 02 30 0E D0 D1 A9 6F 8D BB C0 A9 C2 8D BC C0 D0 C5
60 C5 69 90 0A F0 5D E5 69 85 5F A9 C6 D0 09 49 FF 38 65 69 85 5F A9 E6 8D 1C
C1 8D 23 C1 8D 3E C1 A5 6A C5 5E 90 21 F0 12 E5 5E C5 5F 90 12 4A C5 5F B0 02
C6 FD A6 5E E8 D0 33 C6 FD A6 5E D0 2D 0A C5 5F B0 EE 90 F3 49 FF 38 65 5E C5
5F 90 0C 4A C5 5F B0 02 C6 FD A6 5E CA D0 11 0A C5 5F B0 F4 90 D7 A5 6A C5 5E
90 EE F0 D1 B0 C8 20 F0 E9 A9 30 A4 FD 91 D1 60 AD 19 D4 A4 FB 20 4E C0 84 FB
85 5F 18 6D 00 D0 8D 00 D0 6A 45 5F 10 08 A9 01 4D 10 D0 8D 10 D0 AD 10 D0 4A
AD 00 D0 B0 08 C9 18 B0 16 A9 18 D0 0F C9 58 90 0E 24 5F 10 05 CE 10 D0 B0 EF
A9 57 8D 00 D0 AD 1A D4 A4 FC 20 4E C0 84 FC 49 FF 85 5F 38 6D 01 D0 8D 01 D0
6A 45 5F 10 06 24 5F 10 11 30 07 AD 01 D0 C9 32 B0 04 A9 32 D0 06 C9 FA 90 05
A9 F9 8D 01 D0 A5 69 85 6B A5 6A 85 6C AD 10 D0 4A AD 00 D0 6A 38 E9 0C 4A 4A
85 69 AD 01 D0 38 E9 32 4A 4A 4A 85 6A AD 01 DC 29 10 C5 6D F0 0B 85 6D 29 10
D0 05 20 6C C0 30 10 A5 5E 30 46 A5 69 C5 6B D0 06 A5 6A C5 6C F0 3A A6 5E CA
86 5F A9 03 85 02 A6 5F 20 F0 E9 A9 20 A2 03 A4 5C 88 91 D1 C8 CA D0 FA A2 03
A4 5D 88 91 D1 C8 CA D0 FA E6 5F C6 02 D0 DD A5 5C 85 FD 20 E9 C0 A5 5D 85 FD
20 E9 C0 4C 31 EA 80 C0 E0 F0 F8 FC F0 D8 18 0C 0C 2E 2D 2D 2D 2E 5D 20 20 20
5D 27 2D 2D 2D 27


In action:

No online demo, sorry, because there's AFAIK no js C64 emulator supporting a mouse. If you want to try it yourself, grab VICE, download the binary executable and start it in the C64 emulator:

x64sc -autoload xeyes.prg -controlport1device 3 -keybuf 'sys49152\n'


To grab/ungrab the mouse input in the running emulator, use ctrl+m on Unix/Linux and ctrl+q on windows.

Yes, this had to be done ;) After all, there is an original Commodore mouse for the C64, but of course, the builtin operating system doesn't support it, so I first needed a mouse driver, which already took 230 bytes (including a mouse-cursor shaped hardware sprite and bounds checking code for the screen area, but without translating the pointer coordinates to text screen coordinates).

• To safe some bytes, I decided to keep the OS' IRQ working and use a few Kernal routines where possible (clearing the screen and getting a base pointer for a text screen row).
• The code also puts all variables in zeropage, which saves some more bytes, but destroys floating point values used by BASIC. As the program never exits anyways, this doesn't matter.
• The third trick to reduce size is self modification: There's only code to check for putting the pupil on the left side of the eye. The same code is reused after patching some decrement instructions to increment instructions for the right side.

If you're interested, you can read the code as assembly source here :)

• I seem to be the only one trying to compete here from time to time with C64 code. Loved this challenge, because a mouse on the C64 is something "exotic"! If anyone wonders why I'm less active lately, this is the reason: csdb.dk/release/?id=161435 -- finally trying to do a full-featured game for the C64 :) – Felix Palmen Jan 19 '18 at 0:14
• Just for the fun of it, I did a "deluxe version": csdb.dk/release/?id=161762 – Felix Palmen Jan 29 '18 at 10:15

Clean, 1014904892884840814782772 769 bytes

-6 bytes if the eyes don't need to snap to a grid

This wasn't easy. UIs in functional languages rarely are.

import StdEnv,StdIO,osfont,ostoolbox
a=toReal
c=1>0
Start w#(d,w)=openId w
#(t,w)=worldGetToolbox w
#(_,f,_)=osSelectfont("Courier",[],9)t
=let$p#(s,p)=accPIO getProcessWindowSize p =snd(openWindow NilLS(Window""NilLS[WindowId d,WindowMouse(\_=c)Able(noLS1@),WindowViewSize s,WindowPen[PenFont f]])p);@(MouseUp p _)s={s&ls=p};@(MouseMove p _)s=:{ls={x,y},io}={s&io=setWindowLook d c(c,(\_{newFrame}i#(w,i)=getFontCharWidth f' '(unfill newFrame i) =let g v=let m=y-p.y;n=p.x-x-v*w;s=abs(a m/a n);k|abs m<9&&abs n<w=5|s<0.4142=if(n>0)6 4=sign if(s>2.4143)0n+if(m>0)2 8in[".---.":["|"+++{if(k==e)'0'' '\\e<-[j..j+2]}+++"|"\\j<-[1,4,7]]]++["'---'"]in foldr(\e=drawAt{x=(x/w-5)*w,y=(y/9+e-2)*9}([a+++" "+++b\\a<-g -3&b<-g 3]!!e))i[0..4]))io};@_ s=s in startIO SDI zero$[]w


Make sure that you're using iTasks Clean, have the Courier font installed, and have StdLib BEFORE any subfolders of ObjectIO in the module search path.

Compile with (example, may differ): clm -IL StdLib -IL ObjectIO -IL "ObjectIO/OS <YOUR_OS_HERE>" -IL Dynamics -IL Generics -IL Platform -nci <MODULE_NAME_HERE>

If you've never run Clean before, expect this project to take 5+ minutes to compile.

Ungolfed:

module main
import StdEnv,StdIO,osfont,ostoolbox
height=9
SlopeFor225 :== 0.4142

StartSize :== 8

Universe :== {corner1={x=0,y=0},corner2={x=1,y=1}}

Start :: *World -> *World
Start world = startConsole (openIds 1 world)

startConsole :: ([Id],*World) -> *World
startConsole ([windowID],world)
# (toolbox,world) = worldGetToolbox world
# (_,font,toolbox) = osSelectfont ("Consolas",[],height) toolbox
= startIO SDI {x=0,y=0} (initialise font) [ProcessClose closeProcess] world
where
initialise font pst
# (size,pst) = accPIO getProcessWindowSize pst
# (error,pst) = openWindow undef (window font size) pst
| error<>NoError = abort "bad window"
= pst

window font size
= Window "Xeyes" NilLS
[WindowId           windowID
,WindowClose        (noLS closeProcess)
,WindowMouse        mouseFilter Able (noLS1 track)
,WindowViewDomain   Universe//(getViewDomain StartSize)
,WindowViewSize     size
,WindowPen          [PenFont font]
]

track (MouseDown pos _ _) state=:{ls=point=:{x,y},io}
# point = pos
// move to mouse position
= {state & ls=pos}

track (MouseMove pos _) state=:{ls=point=:{x,y},io}
//redraw to point at mouse
# io = setWindowLook windowID True (True, look) io
= {state & ls=point,io=io}
where
look _ {newFrame} picture
# picture = unfill newFrame picture
# (width,picture) = getPenFontCharWidth' 'picture
= let
determineSector u
# yDist = (y - pos.y)
# xDist = (pos.x - u)
# slope = abs(toReal yDist / toReal xDist)
| (abs yDist) < height && (abs xDist) < width = '9'
| slope < SlopeFor225 = if(xDist > 0) '1' '5'
| yDist > 0
| slope > (2.0+SlopeFor225) = '7'
= if(xDist > 0) '8' '6'
| slope > (2.0+SlopeFor225) = '3'
= if(xDist > 0) '2' '4'
getEye u=map(map(\e|isDigit e=if(e==determineSector(x+u*width))'0'' '=e))[['.---.'],['|678|'],['|591|'],['|432|'],['\'---\'']]
in foldr(\i pic=drawAt{x=(x/width-5)*width,y=(y/height+i-2)*height}([toString(a++[' ':b])\\a<-getEye -3&b<-getEye 3]!!i)pic)picture[0..4]

mouseFilter (MouseDown _ _ _) = True
mouseFilter (MouseMove _ _) = True
mouseFilter _ = False


As you can see from the ungolfed version, most of the code is just setting up the combination of "monospaced font" with "respond to the mouse". And even though Courier doesn't make it easy to tell, it is actually drawing the .s and 's. Swapping to something like Consolas makes it clearer.

• I don't know Clean at all, so perhaps I'm saying something weird, but is it possible to change (abs m)<9&&(abs n)<w='9' to (abs m)<9&(abs n)<w='9'? Also, I suggest adding a screen-to-gif instead of screenshot. – Kevin Cruijssen Jan 17 '18 at 8:23
• @KevinCruijssen That wouldn't work for multiple reasons, but I did save 4 bytes dropping the brackets in the same expression, so thanks! I've also added a screen gif! – Οurous Jan 17 '18 at 8:37

Ruby, 335 + 13 = 348 bytes

+13 bytes for -rio/console flag to enable IO#getch.

Contains literal ESC (0x1b) characters, shown as ␛ below. xxd dump follows.

Caution: This does not clean up after itself at exit. See note under xxd dump below.

include Math
$><<"␛[?1003h" s="" (s<<STDIN.getch ($><<"␛[2J"
x,y=$3.ord-32,$4.ord-32
u,v=x,y if$2 u&&[x-u+3,x-u-3].map{|a|b=y-v e=4*asin(b/sqrt(a**2+b**2))/PI printf"␛[%d;%dH.---.@|567|@|480|@|321|@'---'".gsub(/(#{(a<0?4-e:b<0?8+e:e).round%8rescue 8})|([0-8])|@/){$1?0:$2?" ":"␛[5D␛[1B"},v-2,x-a-2} s="")if /M(C|(#))(.)(.)$/=~s)while 1


Ungolfed

This is a pretty naïve golf of my original Ruby implementation.

include Math       # Saves a few bytes for asin, sqrt, and PI
$> << "␛[?1003h" # Print xterm control sequence to start mouse tracking s = "" # Variable to hold input-so-far ( s << STDIN.getch # Read a character from STDIN ($> << "␛[2J"                     # Clear terminal
x, y = $3.ord - 32,$4.ord - 32  # Get cursor x and y from last match
u, v = x, y if $2 # Update eye position if last matched control sequence was click ("#") u && [x-u+3, x-u-3].map {|a| # For each eye's x-position b = y - v # Eye's y position e = 4 * asin(b / sqrt(a**2 + b**2)) / PI # Convert cursor (x,y) to angle w/ x-axis as 1/8 turns printf "␛[%d;%dH.---.@|567|@|480|@|321|@'---'" # Control code to move text cursor, followed by template for eye .gsub( /(#{ (a < 0 ? 4-e : b < 0 ? 8+e : e).round % 8 rescue 8 # Octant number 0-7 or 8 for center })|([0-8])|@/ ){$1 ? 0 : $2 ? " " : "␛[5D␛[1B" }, # Replace octant number with pupil; other digits with space; and @s with code to move cursor left and down for next line of eye v-2, x-a-2 # (y, x) position of top left corner of eye } s = "" # Clear input-so-far ) if /M(C|(#))(.)(.)$/ =~ s      # ...when input-so-far matches a movement ("C") or click ("#") control sequence
) while 1                        # ...forever


xxd dump

This program turns on mouse tracking with the xterm control sequence \e[?1003h but doesn't turn it off at exit. To turn it off, use the control sequence \e[?1003l, e.g.:

ruby -rio/console visual_eyes.rb; printf '\e[1003l'


Since the program eats all input, it's hard to exit. If you want to be able to exit by pressing Ctrl+C, add the following line below (s<<STDIN.getch:

exit 130 if s.end_with?(?\003)


00000000: 696e 636c 7564 6520 4d61 7468 0a24 3e3c  include Math.$>< 00000010: 3c22 1b5b 3f31 3030 3368 220a 733d 2222 <".[?1003h".s="" 00000020: 0a28 733c 3c53 5444 494e 2e67 6574 6368 .(s<<STDIN.getch 00000030: 0a28 243e 3c3c 221b 5b32 4a22 0a78 2c79 .($><<".[2J".x,y
00000040: 3d24 332e 6f72 642d 3332 2c24 342e 6f72  =$3.ord-32,$4.or
00000050: 642d 3332 0a75 2c76 3d78 2c79 2069 6624  d-32.u,v=x,y if$00000060: 320a 7526 265b 782d 752b 332c 782d 752d 2.u&&[x-u+3,x-u- 00000070: 335d 2e6d 6170 7b7c 617c 623d 792d 760a 3].map{|a|b=y-v. 00000080: 653d 342a 6173 696e 2862 2f73 7172 7428 e=4*asin(b/sqrt( 00000090: 612a 2a32 2b62 2a2a 3229 292f 5049 0a70 a**2+b**2))/PI.p 000000a0: 7269 6e74 6622 1b5b 2564 3b25 6448 2e2d rintf".[%d;%dH.- 000000b0: 2d2d 2e40 7c35 3637 7c40 7c34 3830 7c40 --.@|567|@|480|@ 000000c0: 7c33 3231 7c40 272d 2d2d 2722 2e67 7375 |321|@'---'".gsu 000000d0: 6228 2f28 237b 2861 3c30 3f34 2d65 3a62 b(/(#{(a<0?4-e:b 000000e0: 3c30 3f38 2b65 3a65 292e 726f 756e 6425 <0?8+e:e).round% 000000f0: 3872 6573 6375 6520 387d 297c 285b 302d 8rescue 8})|([0- 00000100: 385d 297c 402f 297b 2431 3f30 3a24 323f 8])|@/){$1?0:$2? 00000110: 2220 223a 221b 5b35 441b 5b31 4222 7d2c " ":".[5D.[1B"}, 00000120: 762d 322c 782d 612d 327d 0a73 3d22 2229 v-2,x-a-2}.s="") 00000130: 6966 202f 4d28 437c 2823 2929 282e 2928 if /M(C|(#))(.)( 00000140: 2e29 242f 3d7e 7329 7768 696c 6520 31 .)$/=~s)while 1