195 lines
8.4 KiB
C#
195 lines
8.4 KiB
C#
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<DoPlayNote> _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<DoPlayNote> 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 });
|
||
}
|
||
}
|
||
}
|