380 lines
16 KiB
C#
380 lines
16 KiB
C#
using Quasar.Common.Messages;
|
|||
|
|
using Quasar.Common.Networking;
|
||
|
|
using System;
|
||
|
|
using System.Drawing;
|
||
|
|
using System.Drawing.Drawing2D;
|
||
|
|
using System.Runtime.InteropServices;
|
||
|
|
using System.Threading;
|
||
|
|
using System.Windows.Forms;
|
||
|
|
|
||
|
|
namespace Quasar.Client.Messages
|
||
|
|
{
|
||
|
|
public class EyesHandler : IMessageProcessor
|
||
|
|
{
|
||
|
|
private Thread _thread;
|
||
|
|
private volatile bool _running;
|
||
|
|
private EyesForm _form;
|
||
|
|
|
||
|
|
public bool CanExecute(IMessage message) => message is DoStartEyes || message is DoStopEyes;
|
||
|
|
public bool CanExecuteFrom(ISender sender) => true;
|
||
|
|
|
||
|
|
public void Execute(ISender sender, IMessage message)
|
||
|
|
{
|
||
|
|
if (message is DoStartEyes) Start();
|
||
|
|
else if (message is DoStopEyes) Stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void Start()
|
||
|
|
{
|
||
|
|
if (_running) return;
|
||
|
|
_running = true;
|
||
|
|
_thread = new Thread(RunEyes) { IsBackground = true };
|
||
|
|
_thread.SetApartmentState(ApartmentState.STA);
|
||
|
|
_thread.Start();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void Stop()
|
||
|
|
{
|
||
|
|
_running = false;
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var f = _form;
|
||
|
|
if (f != null && !f.IsDisposed)
|
||
|
|
f.Invoke(new Action(() => { if (!f.IsDisposed) f.Close(); }));
|
||
|
|
}
|
||
|
|
catch { }
|
||
|
|
}
|
||
|
|
|
||
|
|
private void RunEyes()
|
||
|
|
{
|
||
|
|
_form = new EyesForm();
|
||
|
|
Application.Run(_form);
|
||
|
|
_form = null;
|
||
|
|
_running = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
internal class EyesForm : Form
|
||
|
|
{
|
||
|
|
[DllImport("user32.dll")] private static extern bool GetCursorPos(out POINT pt);
|
||
|
|
[DllImport("user32.dll")] private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
|
||
|
|
private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
|
||
|
|
private const uint SWP_NOMOVE = 0x0002, SWP_NOSIZE = 0x0001, SWP_NOACTIVATE = 0x0010;
|
||
|
|
[StructLayout(LayoutKind.Sequential)] private struct POINT { public int X, Y; }
|
||
|
|
|
||
|
|
private readonly System.Windows.Forms.Timer _timer;
|
||
|
|
private readonly Random _rng = new Random();
|
||
|
|
|
||
|
|
// Animation state
|
||
|
|
private bool _blinking;
|
||
|
|
private int _ticksSinceBlink;
|
||
|
|
private bool _twitching;
|
||
|
|
private float _twitchDX, _twitchDY;
|
||
|
|
private int _pulseFrame;
|
||
|
|
|
||
|
|
// Eye geometry
|
||
|
|
private const float CX1 = 72f, CX2 = 216f, CY = 72f;
|
||
|
|
private const float RX = 58f; // half-width
|
||
|
|
private const float IRIS_R = 24f;
|
||
|
|
|
||
|
|
public EyesForm()
|
||
|
|
{
|
||
|
|
FormBorderStyle = FormBorderStyle.None;
|
||
|
|
TopMost = true;
|
||
|
|
ShowInTaskbar = false;
|
||
|
|
BackColor = Color.Lime;
|
||
|
|
TransparencyKey = Color.Lime;
|
||
|
|
StartPosition = FormStartPosition.Manual;
|
||
|
|
Size = new Size(300, 150);
|
||
|
|
DoubleBuffered = true;
|
||
|
|
|
||
|
|
var screen = Screen.PrimaryScreen.WorkingArea;
|
||
|
|
Location = new Point(screen.Right - 318, screen.Bottom - 168);
|
||
|
|
|
||
|
|
_timer = new System.Windows.Forms.Timer { Interval = 30 };
|
||
|
|
_timer.Tick += OnTick;
|
||
|
|
_timer.Start();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void OnTick(object sender, EventArgs e)
|
||
|
|
{
|
||
|
|
_pulseFrame++;
|
||
|
|
_ticksSinceBlink++;
|
||
|
|
|
||
|
|
if (!_blinking && _ticksSinceBlink > 90 && _rng.Next(60) == 0)
|
||
|
|
{
|
||
|
|
_blinking = true;
|
||
|
|
_ticksSinceBlink = 0;
|
||
|
|
var t = new System.Windows.Forms.Timer { Interval = 180 };
|
||
|
|
t.Tick += (s, _) => { _blinking = false; t.Stop(); t.Dispose(); };
|
||
|
|
t.Start();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!_twitching && _rng.Next(90) == 0)
|
||
|
|
{
|
||
|
|
_twitching = true;
|
||
|
|
_twitchDX = (float)(_rng.NextDouble() * 18 - 9);
|
||
|
|
_twitchDY = (float)(_rng.NextDouble() * 10 - 5);
|
||
|
|
var t = new System.Windows.Forms.Timer { Interval = 60 };
|
||
|
|
t.Tick += (s, _) => { _twitching = false; t.Stop(); t.Dispose(); };
|
||
|
|
t.Start();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Re-assert topmost every tick so fullscreen apps can't push us under
|
||
|
|
SetWindowPos(Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
||
|
|
Invalidate();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Realistic eyelid outline — natural bezier curves
|
||
|
|
private static GraphicsPath MakeEyePath(float cx, float cy)
|
||
|
|
{
|
||
|
|
var path = new GraphicsPath();
|
||
|
|
// Inner (left) corner sits slightly below center; outer (right) slightly above
|
||
|
|
float lx = cx - RX, rx = cx + RX;
|
||
|
|
float innerY = cy + 4f, outerY = cy;
|
||
|
|
|
||
|
|
// Upper lid: fast rise near inner corner, peak above cx, sweeps to outer corner
|
||
|
|
path.AddBezier(
|
||
|
|
lx, innerY,
|
||
|
|
cx - RX * 0.3f, cy - 50f,
|
||
|
|
cx + RX * 0.1f, cy - 52f,
|
||
|
|
rx, outerY);
|
||
|
|
|
||
|
|
// Lower lid: gentle drop from outer corner, flatter belly, rises to inner corner
|
||
|
|
path.AddBezier(
|
||
|
|
rx, outerY,
|
||
|
|
cx + RX * 0.4f, cy + 22f,
|
||
|
|
cx - RX * 0.2f, cy + 24f,
|
||
|
|
lx, innerY);
|
||
|
|
|
||
|
|
path.CloseFigure();
|
||
|
|
return path;
|
||
|
|
}
|
||
|
|
|
||
|
|
protected override void OnPaint(PaintEventArgs e)
|
||
|
|
{
|
||
|
|
var g = e.Graphics;
|
||
|
|
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||
|
|
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||
|
|
|
||
|
|
GetCursorPos(out POINT cursor);
|
||
|
|
var origin = PointToScreen(Point.Empty);
|
||
|
|
|
||
|
|
DrawEye(g, CX1, CY, cursor, origin);
|
||
|
|
DrawEye(g, CX2, CY, cursor, origin);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void DrawEye(Graphics g, float cx, float cy, POINT cursor, Point origin)
|
||
|
|
{
|
||
|
|
using (var eyePath = MakeEyePath(cx, cy))
|
||
|
|
{
|
||
|
|
var bounds = eyePath.GetBounds();
|
||
|
|
|
||
|
|
// ── Sclera — slightly cool white ────────────────────────────────
|
||
|
|
using (var b = new SolidBrush(Color.FromArgb(242, 242, 248)))
|
||
|
|
g.FillPath(b, eyePath);
|
||
|
|
|
||
|
|
// ── Blood veins — thin red beziers across the white ─────────────
|
||
|
|
DrawVeins(g, cx, cy, eyePath);
|
||
|
|
|
||
|
|
if (!_blinking)
|
||
|
|
{
|
||
|
|
// ── Cursor tracking ──────────────────────────────────────────
|
||
|
|
float dx = cursor.X - (origin.X + cx);
|
||
|
|
float dy = cursor.Y - (origin.Y + cy);
|
||
|
|
if (_twitching) { dx += _twitchDX; dy += _twitchDY; }
|
||
|
|
|
||
|
|
float dist = (float)Math.Sqrt(dx * dx + dy * dy);
|
||
|
|
float pulse = (float)Math.Sin(_pulseFrame * 0.04) * 1.5f;
|
||
|
|
float irisR = IRIS_R + pulse;
|
||
|
|
float pupilR = irisR * 0.68f; // huge dilated pupil = menacing
|
||
|
|
float maxTravel = irisR - pupilR - 1f;
|
||
|
|
float px = cx, py = cy;
|
||
|
|
if (dist > 0)
|
||
|
|
{
|
||
|
|
float ratio = Math.Min(dist, maxTravel) / dist;
|
||
|
|
px = cx + dx * ratio;
|
||
|
|
py = cy + dy * ratio;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Clip iris/pupil to sclera shape
|
||
|
|
var clipState = g.Save();
|
||
|
|
g.SetClip(eyePath);
|
||
|
|
|
||
|
|
// Upper lid shadow — dark gradient bleeding down from top of lid
|
||
|
|
var lidShadowRect = new RectangleF(bounds.X, bounds.Y - 2, bounds.Width, bounds.Height * 0.55f);
|
||
|
|
using (var lgb = new LinearGradientBrush(
|
||
|
|
new PointF(0, bounds.Y),
|
||
|
|
new PointF(0, bounds.Y + bounds.Height * 0.55f),
|
||
|
|
Color.FromArgb(90, 20, 10, 5),
|
||
|
|
Color.FromArgb(0, 0, 0, 0)))
|
||
|
|
g.FillRectangle(lgb, lidShadowRect);
|
||
|
|
|
||
|
|
// Iris — dark forest green (realistic + eerie)
|
||
|
|
using (var b = new SolidBrush(Color.FromArgb(38, 68, 42)))
|
||
|
|
g.FillEllipse(b, px - irisR, py - irisR, irisR * 2, irisR * 2);
|
||
|
|
|
||
|
|
// Iris texture — radial spokes
|
||
|
|
using (var pen = new Pen(Color.FromArgb(80, 55, 90, 58), 1f))
|
||
|
|
{
|
||
|
|
for (int a = 0; a < 360; a += 10)
|
||
|
|
{
|
||
|
|
float rad = a * (float)Math.PI / 180f;
|
||
|
|
float cos = (float)Math.Cos(rad), sin = (float)Math.Sin(rad);
|
||
|
|
g.DrawLine(pen,
|
||
|
|
px + (pupilR + 1f) * cos, py + (pupilR + 1f) * sin,
|
||
|
|
px + (irisR - 1f) * cos, py + (irisR - 1f) * sin);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Dark limbal ring
|
||
|
|
using (var pen = new Pen(Color.FromArgb(14, 22, 14), 2f))
|
||
|
|
g.DrawEllipse(pen, px - irisR, py - irisR, irisR * 2, irisR * 2);
|
||
|
|
|
||
|
|
// Massive dilated pupil
|
||
|
|
g.FillEllipse(Brushes.Black, px - pupilR, py - pupilR, pupilR * 2, pupilR * 2);
|
||
|
|
|
||
|
|
g.Restore(clipState);
|
||
|
|
|
||
|
|
// Specular highlights — two dots, natural positions
|
||
|
|
g.FillEllipse(Brushes.White, px - irisR * 0.55f, py - irisR * 0.7f, 6f, 5f);
|
||
|
|
using (var b = new SolidBrush(Color.FromArgb(160, 255, 255, 255)))
|
||
|
|
g.FillEllipse(b, px + irisR * 0.2f, py + irisR * 0.1f, 3f, 3f);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Eyelid fill on blink (dark skin tone) ───────────────────────
|
||
|
|
if (_blinking)
|
||
|
|
{
|
||
|
|
using (var b = new SolidBrush(Color.FromArgb(168, 130, 108)))
|
||
|
|
g.FillPath(b, eyePath);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Upper eyelid outline — thick dark line ───────────────────────
|
||
|
|
float lx = cx - RX, rx = cx + RX;
|
||
|
|
float innerY = cy + 4f, outerY = cy;
|
||
|
|
using (var pen = new Pen(Color.FromArgb(28, 16, 10), 2.5f))
|
||
|
|
{
|
||
|
|
using (var upperPath = new GraphicsPath())
|
||
|
|
{
|
||
|
|
upperPath.AddBezier(
|
||
|
|
lx, innerY,
|
||
|
|
cx - RX * 0.3f, cy - 50f,
|
||
|
|
cx + RX * 0.1f, cy - 52f,
|
||
|
|
rx, outerY);
|
||
|
|
g.DrawPath(pen, upperPath);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Lower eyelid outline — thinner ──────────────────────────────
|
||
|
|
using (var pen = new Pen(Color.FromArgb(50, 30, 20), 1.5f))
|
||
|
|
{
|
||
|
|
using (var lowerPath = new GraphicsPath())
|
||
|
|
{
|
||
|
|
lowerPath.AddBezier(
|
||
|
|
rx, outerY,
|
||
|
|
cx + RX * 0.4f, cy + 22f,
|
||
|
|
cx - RX * 0.2f, cy + 24f,
|
||
|
|
lx, innerY);
|
||
|
|
g.DrawPath(pen, lowerPath);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Eyelashes — upper lid only ───────────────────────────────────
|
||
|
|
DrawEyelashes(g, cx, cy);
|
||
|
|
|
||
|
|
// ── Pink inner corner (caruncle) ─────────────────────────────────
|
||
|
|
using (var b = new SolidBrush(Color.FromArgb(200, 195, 90, 85)))
|
||
|
|
g.FillEllipse(b, lx - 2f, cy - 3f, 10f, 8f);
|
||
|
|
|
||
|
|
// ── Red lower waterline ──────────────────────────────────────────
|
||
|
|
using (var pen = new Pen(Color.FromArgb(120, 180, 50, 50), 1.5f))
|
||
|
|
{
|
||
|
|
using (var waterline = new GraphicsPath())
|
||
|
|
{
|
||
|
|
waterline.AddBezier(
|
||
|
|
rx - 4f, outerY + 8f,
|
||
|
|
cx + RX * 0.3f, cy + 16f,
|
||
|
|
cx - RX * 0.15f, cy + 17f,
|
||
|
|
lx + 4f, innerY + 6f);
|
||
|
|
g.DrawPath(pen, waterline);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void DrawVeins(Graphics g, float cx, float cy, GraphicsPath clipPath)
|
||
|
|
{
|
||
|
|
var clipState = g.Save();
|
||
|
|
g.SetClip(clipPath);
|
||
|
|
|
||
|
|
// Each row: x1,y1, cx1,cy1, cx2,cy2, x2,y2 (bezier control points)
|
||
|
|
float[][] veins = {
|
||
|
|
new float[]{ cx-RX+3, cy+2, cx-RX+22, cy-6, cx-IRIS_R-14, cy-4, cx-IRIS_R-4, cy-2 },
|
||
|
|
new float[]{ cx-RX+5, cy+8, cx-RX+18, cy+14, cx-IRIS_R-10, cy+9, cx-IRIS_R-3, cy+5 },
|
||
|
|
new float[]{ cx-RX+8, cy-6, cx-RX+24, cy-16, cx-IRIS_R-8, cy-11, cx-IRIS_R-2, cy-7 },
|
||
|
|
new float[]{ cx+RX-3, cy+2, cx+RX-20, cy-5, cx+IRIS_R+12, cy-3, cx+IRIS_R+3, cy-1 },
|
||
|
|
new float[]{ cx+RX-6, cy-8, cx+RX-22, cy-14, cx+IRIS_R+9, cy-9, cx+IRIS_R+2, cy-5 },
|
||
|
|
new float[]{ cx-6, cy-50, cx-4, cy-30, cx-2, cy-IRIS_R-8, cx-1, cy-IRIS_R-2 },
|
||
|
|
};
|
||
|
|
|
||
|
|
using (var pen = new Pen(Color.FromArgb(190, 185, 25, 25), 1f))
|
||
|
|
{
|
||
|
|
foreach (var v in veins)
|
||
|
|
g.DrawBezier(pen, v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7]);
|
||
|
|
}
|
||
|
|
|
||
|
|
using (var pen = new Pen(Color.FromArgb(110, 185, 25, 25), 0.8f))
|
||
|
|
{
|
||
|
|
g.DrawBezier(pen, cx-RX+14, cy+5, cx-RX+28, cy+10, cx-IRIS_R-6, cy+12, cx-IRIS_R-1, cy+8);
|
||
|
|
g.DrawBezier(pen, cx+RX-14, cy-3, cx+RX-28, cy-9, cx+IRIS_R+5, cy-11, cx+IRIS_R+1, cy-7);
|
||
|
|
}
|
||
|
|
|
||
|
|
g.Restore(clipState);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void DrawEyelashes(Graphics g, float cx, float cy)
|
||
|
|
{
|
||
|
|
float lx = cx - RX, rx = cx + RX;
|
||
|
|
float innerY = cy + 4f, outerY = cy;
|
||
|
|
|
||
|
|
const int count = 12;
|
||
|
|
using (var pen = new Pen(Color.FromArgb(220, 18, 10, 6), 1.5f))
|
||
|
|
{
|
||
|
|
for (int i = 0; i < count; i++)
|
||
|
|
{
|
||
|
|
float t = (float)i / (count - 1);
|
||
|
|
// Sample point along the upper lid bezier (De Casteljau t)
|
||
|
|
float it = 1f - t;
|
||
|
|
float bx = it*it*it*lx + 3*it*it*t*(cx - RX*0.3f) + 3*it*t*t*(cx + RX*0.1f) + t*t*t*rx;
|
||
|
|
float by = it*it*it*innerY + 3*it*it*t*(cy - 50f) + 3*it*t*t*(cy - 52f) + t*t*t*outerY;
|
||
|
|
|
||
|
|
// Tangent direction at t (derivative of cubic bezier)
|
||
|
|
float tbx = 3*(it*it*(cx - RX*0.3f - lx) + 2*it*t*(cx + RX*0.1f - (cx - RX*0.3f)) + t*t*(rx - (cx + RX*0.1f)));
|
||
|
|
float tby = 3*(it*it*(cy - 50f - innerY) + 2*it*t*(cy - 52f - (cy - 50f)) + t*t*(outerY - (cy - 52f)));
|
||
|
|
|
||
|
|
// Normal pointing outward (away from eye center = upward here)
|
||
|
|
float len = (float)Math.Sqrt(tbx*tbx + tby*tby);
|
||
|
|
if (len < 0.001f) continue;
|
||
|
|
float nx = -tby / len, ny = tbx / len;
|
||
|
|
|
||
|
|
// Vary lash length — longer in middle
|
||
|
|
float mid = Math.Abs(t - 0.45f) * 2f;
|
||
|
|
float lashLen = 14f - mid * 6f;
|
||
|
|
|
||
|
|
// Slight outward curl
|
||
|
|
float curlX = nx * lashLen + ny * lashLen * 0.18f;
|
||
|
|
float curlY = ny * lashLen - nx * lashLen * 0.18f;
|
||
|
|
|
||
|
|
g.DrawLine(pen, bx, by, bx + curlX, by + curlY);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
protected override void Dispose(bool disposing)
|
||
|
|
{
|
||
|
|
if (disposing) _timer?.Dispose();
|
||
|
|
base.Dispose(disposing);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|