Files
2026-08-27 11:22:16 -06:00

195 lines
8.4 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (C4B5). 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 });
}
}
}