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 });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|