using Quasar.Common.Messages; using System; using System.Diagnostics; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; namespace Quasar.Server.Forms { // 2-octave piano (C4–B5). Mouse-click or keyboard to play. // Keyboard layout (standard virtual piano): // White (lower): Z X C V B N M Q W E R T Y U // Black (lower): S D G H J 2 3 5 6 7 public class FrmPiano : Form { private readonly Action _send; // ── Note data ──────────────────────────────────────────────────────── private static readonly string[] _names = { "C4","C#4","D4","D#4","E4","F4","F#4","G4","G#4","A4","A#4","B4","C5","C#5","D5","D#5","E5","F5","F#5","G5","G#5","A5","A#5","B5" }; private static readonly int[] _freqs = { 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988 }; private static readonly bool[] _black = { false,true,false,true,false,false,true,false,true,false,true,false, false,true,false,true,false,false,true,false,true,false,true,false }; // Computer-key mapping (index aligns with _names) private static readonly Keys[] _keyMap = { Keys.Z, Keys.S, Keys.X, Keys.D, Keys.C, Keys.V, Keys.G, Keys.B, Keys.H, Keys.N, Keys.J, Keys.M, Keys.Q, Keys.D2, Keys.W, Keys.D3, Keys.E, Keys.R, Keys.D5, Keys.T, Keys.D6, Keys.Y, Keys.D7, Keys.U }; // ── Layout constants ───────────────────────────────────────────────── private const int WW = 38, WH = 150, BW = 22, BH = 92, PAD = 10, TOP = 32; private readonly Rectangle[] _rects = new Rectangle[24]; private int _pressedIdx = -1; private readonly Stopwatch _slideThrottle = Stopwatch.StartNew(); // Cached brushes private static readonly Color _pressedWhite = Color.FromArgb(100, 180, 255); private static readonly Color _pressedBlack = Color.FromArgb(60, 120, 220); private static readonly Color _bgColor = Color.FromArgb(28, 28, 28); public FrmPiano(Action send) { _send = send; Text = "Piano"; FormBorderStyle = FormBorderStyle.FixedSingle; MaximizeBox = false; StartPosition = FormStartPosition.CenterScreen; KeyPreview = true; DoubleBuffered = true; ClientSize = new Size(PAD * 2 + 14 * WW, TOP + WH + 20); BackColor = _bgColor; BuildRects(); Paint += OnPaint; MouseDown += OnMouseDown; MouseUp += (s, a) => { _pressedIdx = -1; Invalidate(); }; MouseMove += OnMouseMove; KeyDown += OnKeyDown; KeyUp += (s, a) => { _pressedIdx = -1; Invalidate(); }; } // ── Geometry ───────────────────────────────────────────────────────── private void BuildRects() { // White-key index for each note (black keys get -1) int[] whiteIdx = new int[24]; int w = 0; for (int i = 0; i < 24; i++) whiteIdx[i] = _black[i] ? -1 : w++; // White key rects for (int i = 0; i < 24; i++) if (!_black[i]) _rects[i] = new Rectangle(PAD + whiteIdx[i] * WW, TOP, WW, WH); // Black key rects: centered on the gap between the two adjacent white keys // Left white-key note index for each black note int[] leftWhite = { 0, 2, 5, 7, 9, 12, 14, 17, 19, 21 }; int[] blackNote = { 1, 3, 6, 8, 10, 13, 15, 18, 20, 22 }; for (int b = 0; b < blackNote.Length; b++) { int lw = whiteIdx[leftWhite[b]]; _rects[blackNote[b]] = new Rectangle(PAD + (lw + 1) * WW - BW / 2, TOP, BW, BH); } } // ── Painting ───────────────────────────────────────────────────────── private void OnPaint(object sender, PaintEventArgs e) { Graphics g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; // Header label using (var f = new Font("Segoe UI", 9.5f, FontStyle.Bold)) g.DrawString("Piano · Z X C V B N M | Q W E R T Y U", f, Brushes.Silver, PAD, 6); // White keys first for (int i = 0; i < 24; i++) { if (_black[i]) continue; bool pressed = _pressedIdx == i; var r = _rects[i]; using (var br = new SolidBrush(pressed ? _pressedWhite : Color.White)) g.FillRectangle(br, r); g.DrawRectangle(Pens.Black, r); // Key label using (var f = new Font("Segoe UI", 6.5f)) { char ch = KeyChar(i); g.DrawString(ch.ToString(), f, Brushes.DimGray, r.X + (WW - 8) / 2, r.Bottom - 17); } } // Black keys on top for (int i = 0; i < 24; i++) { if (!_black[i]) continue; bool pressed = _pressedIdx == i; var r = _rects[i]; using (var br = new SolidBrush(pressed ? _pressedBlack : Color.FromArgb(28, 28, 28))) g.FillRectangle(br, r); // Subtle highlight gradient on un-pressed black key if (!pressed) { using (var lgb = new LinearGradientBrush(r, Color.FromArgb(80, 80, 80), Color.FromArgb(28, 28, 28), LinearGradientMode.Horizontal)) g.FillRectangle(lgb, new Rectangle(r.X, r.Y, 5, r.Height)); } g.DrawRectangle(Pens.Black, r); // Key label using (var f = new Font("Segoe UI", 5.5f)) { char ch = KeyChar(i); g.DrawString(ch.ToString(), f, Brushes.DimGray, r.X + 4, r.Bottom - 14); } } } private char KeyChar(int idx) { var k = _keyMap[idx]; if (k >= Keys.D0 && k <= Keys.D9) return (char)('0' + (k - Keys.D0)); return (char)k; // letter keys: Keys.A = 65 = 'A' } // ── Input ───────────────────────────────────────────────────────────── private void OnMouseDown(object sender, MouseEventArgs e) { // Black keys sit on top — test them first for (int i = 0; i < 24; i++) if (_black[i] && _rects[i].Contains(e.Location)) { Press(i); return; } for (int i = 0; i < 24; i++) if (!_black[i] && _rects[i].Contains(e.Location)) { Press(i); return; } } private void OnMouseMove(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.None) return; // Throttle slide to avoid flooding the connection if (_slideThrottle.ElapsedMilliseconds < 80) return; for (int i = 0; i < 24; i++) if (_black[i] && _rects[i].Contains(e.Location)) { if (_pressedIdx != i) { _slideThrottle.Restart(); Press(i); } return; } for (int i = 0; i < 24; i++) if (!_black[i] && _rects[i].Contains(e.Location)) { if (_pressedIdx != i) { _slideThrottle.Restart(); Press(i); } return; } } private void OnKeyDown(object sender, KeyEventArgs e) { for (int i = 0; i < _keyMap.Length; i++) if (_keyMap[i] == e.KeyCode) { if (_pressedIdx != i) Press(i); return; } } private void Press(int idx) { _pressedIdx = idx; Invalidate(); _send(new DoPlayNote { Frequency = _freqs[idx], DurationMs = 1000 }); } } }