├── icon.ico ├── icon_new.ico ├── SharpExtension.dll ├── SharpCollections.dll ├── App.config ├── Structures.cs ├── UI ├── BBinding.cs ├── Colors.xaml ├── HexColorPicker.xaml ├── OrValueConverter.cs ├── ValueSlider.xaml ├── InplaceConverter.cs ├── BetterRadio.xaml ├── BetterCheckbox.xaml ├── BetterSlider.xaml ├── NumberSelect.xaml ├── BetterCheckbox.xaml.cs ├── ListBox.xaml ├── HexColorPicker.xaml.cs ├── RippleEffectDecorator.cs ├── BetterRadio.xaml.cs ├── ValueSlider.xaml.cs ├── Scrollbar.xaml ├── NumberSelect.xaml.cs ├── BetterSlider.xaml.cs └── Material.xaml ├── README.md ├── Program.cs ├── Core ├── RendererBase.cs ├── FFMpeg.cs ├── RenderOptions.cs ├── Color.cs ├── Global.cs ├── NoteSorter.cs ├── CanvasBase.cs ├── RenderFile.cs ├── CommonRenderer.cs └── CommonCanvas.cs ├── Config.cs ├── PFAConfigrationLoader.cs └── CustomColor.cs /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/null7323/QMidiCore-Quaver-Stream-Renderer/HEAD/icon.ico -------------------------------------------------------------------------------- /icon_new.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/null7323/QMidiCore-Quaver-Stream-Renderer/HEAD/icon_new.ico -------------------------------------------------------------------------------- /SharpExtension.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/null7323/QMidiCore-Quaver-Stream-Renderer/HEAD/SharpExtension.dll -------------------------------------------------------------------------------- /SharpCollections.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/null7323/QMidiCore-Quaver-Stream-Renderer/HEAD/SharpCollections.dll -------------------------------------------------------------------------------- /App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Structures.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace QQS_UI 8 | { 9 | public struct Note 10 | { 11 | public byte Key; 12 | public ushort Track; 13 | public uint Start, End; 14 | } 15 | public struct Tempo 16 | { 17 | public uint Tick; 18 | public uint Value; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /UI/BBinding.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Data; 8 | 9 | namespace QQS_UI.UI 10 | { 11 | public class BBinding : Binding 12 | { 13 | public BBinding(DependencyProperty dp, object source) : base() 14 | { 15 | Path = new PropertyPath(dp); 16 | Source = source; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QMidiCore-Quaver-Stream-Renderer 2 | QMidiCore Quaver Stream Renderer (简称QQS) 是一个黑乐谱渲染器. 原作者是qishipai. 3 | 4 | ### 项目引用 Project reference 5 | - SharpExtension 提供一些操作非托管内存以及流的方法. Provides some methods of allocating, freeing unmanaged memory and stream operations. 6 | - SharpCollections 提供一些基于非托管内存的集合类型. Provides some collections using unmanaged memory. 7 | - Newtonsoft.Json 进行Json操作. 8 | 9 | ### UI 10 | 使用了Arduano的"Zenith-MIDI"项目中的UI样式,并进行了颜色修改. 11 | 12 | ### Contributors 13 | - qishipai (原作者) 14 | - Tweak 15 | - fnull601 16 | - MBMS (翻译) 17 | 18 | ### 说明 19 | 平台:.NET 7.0-Windows 20 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | 4 | namespace QQS_UI 5 | { 6 | public class Program 7 | { 8 | [STAThread] 9 | private static unsafe void Main(string[] args) 10 | { 11 | Console.Title = "QMIDICore Quaver Stream Renderer"; 12 | #if !DEBUG 13 | try 14 | { 15 | #endif 16 | Application app = new(); 17 | _ = app.Run(new MainWindow()); 18 | #if !DEBUG 19 | } 20 | catch (Exception ex) 21 | { 22 | Console.WriteLine($"An error occurred:\n{ex.Message}\nStack Trace:\n{ex.StackTrace}"); 23 | } 24 | #endif 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /UI/Colors.xaml: -------------------------------------------------------------------------------- 1 | 7 | #0E8DB2 8 | 9 | 10 | Microsoft Yahei Light 11 | -------------------------------------------------------------------------------- /UI/HexColorPicker.xaml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /UI/OrValueConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Windows.Data; 4 | 5 | namespace QQS_UI.UI 6 | { 7 | internal class OrValueConverter : IMultiValueConverter 8 | { 9 | public object Convert(object[] values, Type targetType, object parmeter, CultureInfo culture) 10 | { 11 | foreach (object obj in values) 12 | { 13 | if ((bool)obj) 14 | { 15 | return true; 16 | } 17 | } 18 | return false; 19 | } 20 | 21 | public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) 22 | { 23 | throw new NotImplementedException(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /UI/ValueSlider.xaml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /UI/InplaceConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | using System.Windows.Data; 9 | using System.Reflection; 10 | 11 | namespace QQS_UI.UI 12 | { 13 | public class InplaceConverter : IMultiValueConverter 14 | { 15 | Func func; 16 | Binding[] bindings; 17 | 18 | public InplaceConverter(Binding[] bindings, Func func) 19 | { 20 | this.bindings = bindings; 21 | this.func = func; 22 | } 23 | 24 | public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) 25 | { 26 | return func(values); 27 | } 28 | 29 | public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) 30 | { 31 | throw new NotImplementedException(); 32 | } 33 | 34 | public void Set(FrameworkElement o, DependencyProperty p) 35 | { 36 | var b = new MultiBinding(); 37 | b.Converter = this; 38 | foreach (var _b in bindings) b.Bindings.Add(_b); 39 | o.SetBinding(p, b); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Core/RendererBase.cs: -------------------------------------------------------------------------------- 1 | using SharpExtension.Collections; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace QQS_UI.Core 9 | { 10 | public abstract class RendererBase 11 | { 12 | protected readonly RenderFile renderFile; 13 | protected readonly UnmanagedList[] noteMap; 14 | protected readonly UnmanagedList tempos; 15 | 16 | protected readonly ushort ppq; 17 | protected readonly double noteSpeed; 18 | protected readonly int fps; 19 | protected readonly uint height; 20 | protected readonly uint keyHeight; 21 | protected readonly bool isTickBased; 22 | protected readonly bool isPreview; 23 | 24 | public bool Interrupt = false; 25 | public RendererBase(RenderFile file, in RenderOptions options) 26 | { 27 | renderFile = file; 28 | noteMap = file.Notes; 29 | tempos = file.Tempos; 30 | 31 | ppq = file.Division; 32 | noteSpeed = options.NoteSpeed; 33 | fps = options.FPS; 34 | height = (uint)options.Height; 35 | keyHeight = (uint)options.KeyHeight; 36 | isTickBased = options.TickBased; 37 | isPreview = options.PreviewMode; 38 | } 39 | 40 | public abstract void Render(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /UI/BetterRadio.xaml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Core/FFMpeg.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.CompilerServices; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using SharpExtension.IO; 8 | 9 | namespace QQS_UI.Core 10 | { 11 | /// 12 | /// 简单地封装操作ffmpeg的逻辑. 13 | /// 14 | public readonly unsafe struct FFMpeg : IDisposable 15 | { 16 | private readonly CStream stream; 17 | private readonly ulong frameSize; 18 | /// 19 | /// 初始化一个新的 实例. 20 | /// 21 | /// 初始化ffmpeg的参数. 22 | /// 输入视频的宽. 23 | /// 输入视频的高. 24 | public FFMpeg(string ffargs, int width, int height) 25 | { 26 | string ffcommand; 27 | stream = CStream.OpenPipe(ffcommand = "ffmpeg " + ffargs, "wb"); 28 | frameSize = (uint)width * (uint)height * 4; 29 | Console.WriteLine("FFMpeg 启动命令: {0}", ffcommand); 30 | } 31 | /// 32 | /// 向 FFMpeg 写入一帧.
33 | /// Write a frame to FFMpeg. 34 | ///
35 | /// 存有视频画面的缓冲区. 36 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 37 | public void WriteFrame(in void* buffer) 38 | { 39 | _ = stream.WriteWithoutLock(buffer, frameSize, 1); 40 | } 41 | 42 | public void Dispose() 43 | { 44 | if (!stream.Closed) 45 | { 46 | _ = stream.Close(); 47 | } 48 | GC.SuppressFinalize(this); 49 | } 50 | //~FFMpeg() 51 | //{ 52 | // stream.Dispose(); 53 | //} 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Config.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text.Json; 6 | using System.Threading.Tasks; 7 | 8 | namespace QQS_UI 9 | { 10 | public struct DialogPath 11 | { 12 | public string MidiDirectory; 13 | public string VideoDirectory; 14 | public string ColorDirectory; 15 | } 16 | /// 17 | /// 表示用于存储Midi文件夹路径和视频文件夹路径. 18 | /// 19 | public class Config 20 | { 21 | private DialogPath ConfigPath; 22 | private readonly string ConfigName; 23 | /// 24 | /// 初始化一个新的 实例. 25 | /// 26 | /// 配置文件的文件名, 需要".json"后缀. 27 | public Config(string configName = "config.json") 28 | { 29 | ConfigName = configName; 30 | if (File.Exists(configName)) 31 | { 32 | try 33 | { 34 | string jsonData = File.ReadAllText(configName); 35 | ConfigPath = JsonSerializer.Deserialize(jsonData); 36 | } 37 | catch 38 | { 39 | File.Create(ConfigName).Close(); 40 | ConfigPath = new DialogPath(); 41 | } 42 | } 43 | else 44 | { 45 | File.Create(ConfigName).Close(); 46 | ConfigPath = new DialogPath(); 47 | } 48 | } 49 | public string CachedVideoDirectory 50 | { 51 | get => ConfigPath.VideoDirectory; 52 | set => ConfigPath.VideoDirectory = value; 53 | } 54 | 55 | public string CachedColorDirectory 56 | { 57 | get => ConfigPath.ColorDirectory; 58 | set => ConfigPath.ColorDirectory = value; 59 | } 60 | 61 | public string CachedMIDIDirectory 62 | { 63 | get => ConfigPath.MidiDirectory; 64 | set => ConfigPath.MidiDirectory = value; 65 | } 66 | 67 | public void SaveConfig() 68 | { 69 | File.WriteAllText(ConfigName, JsonSerializer.Serialize(ConfigPath)); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /UI/BetterCheckbox.xaml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 28 | 29 | 30 | 31 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /PFAConfigrationLoader.cs: -------------------------------------------------------------------------------- 1 | using QQS_UI.Core; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using System.Xml; 9 | 10 | namespace QQS_UI 11 | { 12 | public static class PFAConfigrationLoader 13 | { 14 | public static string ConfigurationPath 15 | { 16 | get 17 | { 18 | string path = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); 19 | path += "\\Piano From Above\\Config.xml"; 20 | return File.Exists(path) ? path : null; 21 | } 22 | } 23 | 24 | /// 25 | /// 判断PFA配置文件是否可用.
26 | /// Determines whether PFA configuration is available. 27 | ///
28 | public static bool IsConfigurationAvailable => ConfigurationPath != null; 29 | 30 | /// 31 | /// 加载 PFA Config的颜色.
32 | /// Load colors from PFA configuration if possible. 33 | ///
34 | /// 35 | /// 如果无法加载配置, 返回;
36 | /// 如果加载成功, 则以数组的形式返回这些颜色.
37 | /// If it fails to load PFA configuration, will be returned.
38 | /// If it succeeds in loading config, then an array containing these colors will be returned. 39 | ///
40 | public static RGBAColor[] LoadPFAConfigurationColors() 41 | { 42 | if (!IsConfigurationAvailable) 43 | { 44 | return null; 45 | } 46 | XmlDocument doc = new(); 47 | doc.Load(ConfigurationPath); 48 | XmlNode rootNode = doc.SelectSingleNode("PianoFromAbove"); 49 | XmlNode visualNode = rootNode.SelectSingleNode("Visual"); 50 | XmlNode colors = visualNode.SelectSingleNode("Colors"); 51 | XmlNodeList actualColors = colors.SelectNodes("Color"); 52 | List retColors = new(); 53 | foreach (XmlNode node in actualColors) 54 | { 55 | byte r = byte.Parse(node.Attributes[0].Value); 56 | byte g = byte.Parse(node.Attributes[1].Value); 57 | byte b = byte.Parse(node.Attributes[2].Value); 58 | retColors.Add(new RGBAColor 59 | { 60 | R = r, 61 | G = g, 62 | B = b, 63 | A = 0xFF 64 | }); 65 | } 66 | Console.WriteLine("PFA 配置颜色解析完成. 一共 {0} 种颜色.", retColors.Count); 67 | return retColors.ToArray(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Core/RenderOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.CompilerServices; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace QQS_UI.Core 9 | { 10 | /// 11 | /// 表示渲染的设置. 请使用 来创建一个 结构. 12 | /// 13 | public struct RenderOptions 14 | { 15 | /// 16 | /// 此成员没有被使用过.
17 | /// This member is wasted. 18 | ///
19 | public bool TickBased; 20 | public bool TransparentBackground; 21 | public bool ThinnerNotes; 22 | public bool DrawSeparator; 23 | public bool PreviewMode; 24 | public bool DrawGreySquare; 25 | public bool Gradient; 26 | public bool BetterBlackKeys; 27 | public bool WhiteKeyShade; 28 | public bool BrighterNotesOnHit; 29 | public int Width, Height, FPS, VideoQuality, KeyHeight, PressedNotesShadeDecrement; 30 | public VideoQualityOptions QualityOptions; 31 | public VerticalGradientDirection KeyboardGradientDirection; 32 | public VerticalGradientDirection SeparatorGradientDirection; 33 | public HorizontalGradientDirection NoteGradientDirection; 34 | public RGBAColor DivideBarColor; 35 | public RGBAColor BackgroundColor; 36 | public double NoteSpeed; 37 | public double DelayStartSeconds; 38 | public string Input; 39 | public string Output; 40 | public string AdditionalFFMpegArgument; 41 | public static RenderOptions CreateRenderOptions() 42 | { 43 | return new RenderOptions 44 | { 45 | Width = 1920, 46 | Height = 1080, 47 | FPS = 60, 48 | VideoQuality = 17, 49 | KeyHeight = 162, 50 | NoteSpeed = 1, 51 | DivideBarColor = 0xFF0000A0, 52 | TickBased = true, 53 | TransparentBackground = false, 54 | WhiteKeyShade = true, 55 | DrawSeparator = true, 56 | PreviewMode = false, 57 | AdditionalFFMpegArgument = string.Empty, 58 | DrawGreySquare = false, 59 | Gradient = true, 60 | ThinnerNotes = true, 61 | DelayStartSeconds = 0, 62 | PressedNotesShadeDecrement = 0, // 0 means this option does not work. 63 | BrighterNotesOnHit = false, 64 | BetterBlackKeys = true, 65 | KeyboardGradientDirection = VerticalGradientDirection.FromButtomToTop, 66 | NoteGradientDirection = HorizontalGradientDirection.FromLeftToRight, 67 | SeparatorGradientDirection = VerticalGradientDirection.FromButtomToTop, 68 | QualityOptions = VideoQualityOptions.CRF, 69 | BackgroundColor = new RGBAColor 70 | { 71 | A = 0xFF, 72 | G = 0, 73 | R = 0, 74 | B = 0 75 | } 76 | }; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /UI/BetterSlider.xaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /Core/Color.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.CompilerServices; 5 | using System.Runtime.InteropServices; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace QQS_UI.Core 10 | { 11 | /// 12 | /// 表示一个RGBA颜色结构.
13 | /// Represents a color structure of RGBA. 14 | ///
15 | [StructLayout(LayoutKind.Sequential)] 16 | public struct RGBAColor 17 | { 18 | public static readonly RGBAColor White = 0xFFFFFFFF; 19 | public static readonly RGBAColor Black = 0x00000000; 20 | 21 | public byte R; 22 | public byte G; 23 | public byte B; 24 | public byte A; 25 | public unsafe RGBAColor(uint color) 26 | { 27 | fixed (RGBAColor* instance = &this) 28 | { 29 | // 直接初始化. Initialize 'this' directly. 30 | // 可以直接复制是因为 uint 是以小端序存储. It is ok to assign in this way, for unsigned int is little endian. 31 | Buffer.MemoryCopy(&color, instance, 4, 4); 32 | } 33 | } 34 | public RGBAColor(byte r, byte g, byte b, byte a) 35 | { 36 | R = r; 37 | G = g; 38 | B = b; 39 | A = a; 40 | } 41 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 42 | public static unsafe implicit operator uint(RGBAColor color) 43 | { 44 | return *(uint*)&color; 45 | } 46 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 47 | public static unsafe implicit operator RGBAColor(uint color) 48 | { 49 | return new RGBAColor(color); 50 | } 51 | public static unsafe explicit operator YUVColor(RGBAColor col) 52 | { 53 | return new YUVColor(col); 54 | } 55 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 56 | public static RGBAColor MixColors(RGBAColor foreground, RGBAColor background, byte alpha) 57 | { 58 | double ratio = alpha / 255.0; 59 | background.R += (byte)Math.Round((foreground.R - background.R) * ratio); 60 | background.G += (byte)Math.Round((foreground.G - background.G) * ratio); 61 | background.B += (byte)Math.Round((foreground.B - background.B) * ratio); 62 | return background; 63 | } 64 | } 65 | 66 | [StructLayout(LayoutKind.Sequential)] 67 | public struct YUVColor 68 | { 69 | public float Y; 70 | public float U; 71 | public float V; 72 | public YUVColor(RGBAColor rgba) // alpha is ignored. 73 | { 74 | Y = (0.299F * rgba.R) + (0.587F * rgba.G) + (0.114F * rgba.B); 75 | U = (-0.147F * rgba.R) + (-0.289F * rgba.G) + (-0.436F * rgba.B); 76 | V = (0.615F * rgba.R) + (-0.515F * rgba.G) + (-0.1F * rgba.B); 77 | } 78 | public static explicit operator RGBAColor(in YUVColor yuv) 79 | { 80 | return new RGBAColor 81 | { 82 | A = 255, 83 | R = (byte)(yuv.Y + (1.140F * yuv.U)), 84 | G = (byte)(yuv.Y - (0.395F * yuv.U) - (0.581F * yuv.V)), 85 | B = (byte)(yuv.Y + (2.032F * yuv.U)) 86 | }; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /UI/NumberSelect.xaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 31 | 32 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 71 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /UI/BetterCheckbox.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | using System.Windows.Controls; 9 | using System.Windows.Data; 10 | using System.Windows.Documents; 11 | using System.Windows.Input; 12 | using System.Windows.Media; 13 | using System.Windows.Media.Animation; 14 | using System.Windows.Media.Imaging; 15 | using System.Windows.Navigation; 16 | using System.Windows.Shapes; 17 | 18 | namespace QQS_UI.UI 19 | { 20 | /// 21 | /// Interaction logic for BetterCheckbox.xaml 22 | /// 23 | public partial class BetterCheckbox : UserControl 24 | { 25 | public bool IsChecked 26 | { 27 | get { return (bool)GetValue(IsCheckedProperty); } 28 | set { SetValue(IsCheckedProperty, value); } 29 | } 30 | 31 | public static readonly DependencyProperty IsCheckedProperty = 32 | DependencyProperty.Register("IsChecked", typeof(bool), typeof(BetterCheckbox), new PropertyMetadata(false, (s, e) => ((BetterCheckbox)s).OnCheckChanged())); 33 | 34 | 35 | public string Text 36 | { 37 | get { return (string)GetValue(TextProperty); } 38 | set { SetValue(TextProperty, value); } 39 | } 40 | 41 | public static readonly DependencyProperty TextProperty = 42 | DependencyProperty.Register("Text", typeof(string), typeof(BetterCheckbox), new PropertyMetadata("")); 43 | 44 | 45 | public static readonly RoutedEvent CheckToggledEvent = EventManager.RegisterRoutedEvent( 46 | "RadioChecked", RoutingStrategy.Bubble, 47 | typeof(RoutedPropertyChangedEventHandler), typeof(BetterCheckbox)); 48 | 49 | public event RoutedPropertyChangedEventHandler CheckToggled 50 | { 51 | add { AddHandler(CheckToggledEvent, value); } 52 | remove { RemoveHandler(CheckToggledEvent, value); } 53 | } 54 | 55 | 56 | void OnCheckChanged() 57 | { 58 | RaiseEvent(new RoutedPropertyChangedEventArgs(!IsChecked, IsChecked, CheckToggledEvent)); 59 | } 60 | 61 | 62 | public BetterCheckbox() 63 | { 64 | InitializeComponent(); 65 | new InplaceConverter(new[] { 66 | new BBinding(IsCheckedProperty, this) 67 | }, v => (bool)v[0] ? Visibility.Visible : Visibility.Hidden) 68 | .Set(checkedBox, VisibilityProperty); 69 | } 70 | 71 | private void DockPanel_MouseDown(object sender, MouseButtonEventArgs e) 72 | { 73 | IsChecked = !IsChecked; 74 | 75 | 76 | double ExpandTime = 0.1; 77 | double FadeTime = 0.1; 78 | 79 | double o = 0.7; 80 | 81 | var targetWidth = rippleBox.ActualWidth; 82 | 83 | var ellipse = new Ellipse() 84 | { 85 | Fill = (Brush)Resources["PrimaryBrush"], 86 | HorizontalAlignment = HorizontalAlignment.Center, 87 | VerticalAlignment = VerticalAlignment.Center, 88 | Opacity = o 89 | }; 90 | ellipse.SetBinding(HeightProperty, new Binding("Width") { Source = ellipse }); 91 | 92 | Storyboard storyboard = new Storyboard(); 93 | 94 | var expand = new DoubleAnimation(10, targetWidth, new Duration(TimeSpan.FromSeconds(ExpandTime + FadeTime))); 95 | storyboard.Children.Add(expand); 96 | Storyboard.SetTarget(expand, ellipse); 97 | Storyboard.SetTargetProperty(expand, new PropertyPath(WidthProperty)); 98 | 99 | var opacity = new DoubleAnimation(o, 0, new Duration(TimeSpan.FromSeconds(FadeTime))); 100 | opacity.BeginTime = TimeSpan.FromSeconds(ExpandTime); 101 | storyboard.Children.Add(opacity); 102 | Storyboard.SetTarget(opacity, ellipse); 103 | Storyboard.SetTargetProperty(opacity, new PropertyPath(Ellipse.OpacityProperty)); 104 | 105 | rippleBox.Children.Add(ellipse); 106 | 107 | storyboard.Begin(); 108 | 109 | var waitTime = ExpandTime + FadeTime; 110 | Task.Run(() => 111 | { 112 | Thread.Sleep(TimeSpan.FromSeconds(waitTime)); 113 | Dispatcher.Invoke(() => 114 | { 115 | rippleBox.Children.Remove(ellipse); 116 | }); 117 | }); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /UI/ListBox.xaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /UI/HexColorPicker.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Navigation; 14 | using System.Windows.Shapes; 15 | 16 | namespace QQS_UI.UI 17 | { 18 | /// 19 | /// Interaction logic for HexColorPicker.xaml 20 | /// 21 | public partial class HexColorPicker : UserControl 22 | { 23 | public Color Color 24 | { 25 | get { return (Color)GetValue(ColorProperty); } 26 | set { SetValue(ColorProperty, value); } 27 | } 28 | 29 | public static readonly DependencyProperty ColorProperty = 30 | DependencyProperty.Register("Color", typeof(Color), typeof(HexColorPicker), new PropertyMetadata(Color.FromArgb(255, 255, 255, 255), (s, e) => ((HexColorPicker)s).OnColorPropertyChanged(e))); 31 | 32 | 33 | public bool UseAlpha 34 | { 35 | get { return (bool)GetValue(UseAlphaProperty); } 36 | set { SetValue(UseAlphaProperty, value); } 37 | } 38 | 39 | public static readonly DependencyProperty UseAlphaProperty = 40 | DependencyProperty.Register("UseAlpha", typeof(bool), typeof(HexColorPicker), new PropertyMetadata(false)); 41 | 42 | public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent( 43 | "ValueChanged", RoutingStrategy.Bubble, 44 | typeof(RoutedPropertyChangedEventHandler), typeof(HexColorPicker)); 45 | 46 | public event RoutedPropertyChangedEventHandler ValueChanged 47 | { 48 | add { AddHandler(ValueChangedEvent, value); } 49 | remove { RemoveHandler(ValueChangedEvent, value); } 50 | } 51 | 52 | 53 | void OnColorPropertyChanged(DependencyPropertyChangedEventArgs e) 54 | { 55 | SaveString(); 56 | RaiseEvent(new RoutedPropertyChangedEventArgs((Color)e.OldValue, (Color)e.NewValue, ValueChangedEvent)); 57 | } 58 | 59 | string Hexify(byte val) 60 | { 61 | var s = val.ToString("X"); 62 | if (s.Length == 1) return "0" + s; 63 | return s; 64 | } 65 | 66 | void SaveString() 67 | { 68 | string s = ""; 69 | s += Hexify(Color.R); 70 | s += Hexify(Color.G); 71 | s += Hexify(Color.B); 72 | if (UseAlpha && (hexText.Text.Length != 6 || Color.A != 255)) 73 | s += Hexify(Color.A); 74 | if (hexText.Text != s) 75 | hexText.Text = s; 76 | } 77 | 78 | 79 | public HexColorPicker() 80 | { 81 | InitializeComponent(); 82 | 83 | new InplaceConverter( 84 | new[] { new BBinding(UseAlphaProperty, this) }, 85 | (e) => (bool)e[0] ? 8 : 6 86 | ).Set(hexText, TextBox.MaxLengthProperty); 87 | } 88 | 89 | private void HexText_TextChanged(object sender, TextChangedEventArgs e) 90 | { 91 | if (!((hexText.Text.Length == 6) || (UseAlpha && hexText.Text.Length == 8))) return; 92 | try 93 | { 94 | int col = int.Parse(hexText.Text, System.Globalization.NumberStyles.HexNumber); 95 | Color c; 96 | if (hexText.Text.Length == 8) 97 | c = Color.FromArgb( 98 | (byte)((col >> 0) & 0xFF), 99 | (byte)((col >> 24) & 0xFF), 100 | (byte)((col >> 16) & 0xFF), 101 | (byte)((col >> 8) & 0xFF) 102 | ); 103 | else 104 | c = Color.FromArgb( 105 | 255, 106 | (byte)((col >> 16) & 0xFF), 107 | (byte)((col >> 8) & 0xFF), 108 | (byte)((col >> 0) & 0xFF) 109 | ); 110 | Color = c; 111 | } 112 | catch { } 113 | } 114 | 115 | private void HexText_LostFocus(object sender, RoutedEventArgs e) 116 | { 117 | SaveString(); 118 | } 119 | 120 | private void UserControl_KeyDown(object sender, KeyEventArgs e) 121 | { 122 | if (e.Key == Key.Enter) 123 | { 124 | SaveString(); 125 | e.Handled = true; 126 | Keyboard.ClearFocus(); 127 | 128 | FrameworkElement parent = (FrameworkElement)hexText.Parent; 129 | while (parent != null && parent is IInputElement && !((IInputElement)parent).Focusable) 130 | { 131 | parent = (FrameworkElement)parent.Parent; 132 | } 133 | 134 | DependencyObject scope = FocusManager.GetFocusScope(hexText); 135 | FocusManager.SetFocusedElement(scope, parent as IInputElement); 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /CustomColor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text.Json; 6 | using System.Threading.Tasks; 7 | using QQS_UI.Core; 8 | 9 | #nullable disable 10 | namespace QQS_UI 11 | { 12 | public class CustomColor 13 | { 14 | public RGBAColor[] Colors; 15 | public CustomColor(string colorFileName = "colors.json") 16 | { 17 | if (!File.Exists(colorFileName)) 18 | { 19 | string colors = JsonSerializer.Serialize(Global.KeyColors); 20 | File.WriteAllText(colorFileName, colors); 21 | Colors = new RGBAColor[96]; 22 | Array.Copy(Global.DefaultColors, Colors, 96); 23 | } 24 | else 25 | { 26 | try 27 | { 28 | string colorData = File.ReadAllText(colorFileName); 29 | Colors = JsonSerializer.Deserialize(colorData); 30 | } 31 | catch 32 | { 33 | Console.WriteLine("加载颜色配置时出现错误, 将使用默认颜色..."); 34 | Colors = new RGBAColor[96]; 35 | Array.Copy(Global.DefaultColors, Colors, 96); 36 | } 37 | } 38 | Console.WriteLine("颜色加载完成. 共 {0} 种颜色.", Colors.Length); 39 | } 40 | 41 | private CustomColor() 42 | { 43 | 44 | } 45 | /// 46 | /// 将指定的文件的颜色加载到当前实例中. 47 | /// 48 | /// 颜色文件路径. 49 | /// 如果文件不存在, 返回-1; 如果加载时出现问题, 返回1; 如果没有错误, 返回0. 50 | public int Load(string colorFileName) 51 | { 52 | if (!File.Exists(colorFileName)) 53 | { 54 | return -1; 55 | } 56 | string colorData = File.ReadAllText(colorFileName); 57 | RGBAColor[] lastColors = Colors; 58 | try 59 | { 60 | Colors = JsonSerializer.Deserialize(colorData); 61 | } 62 | catch 63 | { 64 | Colors = lastColors; 65 | return 1; 66 | } 67 | return 0; 68 | } 69 | 70 | /// 71 | /// 将当前实例的颜色拷贝到中.
72 | /// Copy colors owned by current instance to . 73 | ///
74 | /// 75 | /// 请注意: 这不是一个线程安全操作.
76 | /// This is not a thread-safe operation. 77 | ///
78 | /// 79 | /// 如果当前颜色为, 返回-1;
80 | /// 如果当前颜色不为, 但是长度为0, 返回1;
81 | /// 如果操作无异常, 返回0.
82 | /// If colors owned by is null then -1 will be returned;
83 | /// If the color array owned by is not null but its length equals 0, then 1 is returned;
84 | /// If the operation is successful, returns 0. 85 | ///
86 | public int SetGlobal() 87 | { 88 | if (Colors == null) 89 | { 90 | return -1; 91 | } 92 | if (Colors.Length == 0) 93 | { 94 | return 1; 95 | } 96 | Global.KeyColors = new RGBAColor[Colors.Length]; 97 | Global.NoteColors = new RGBAColor[Colors.Length]; 98 | Array.Copy(Colors, Global.KeyColors, Colors.Length); 99 | Array.Copy(Colors, Global.NoteColors, Colors.Length); 100 | return 0; 101 | } 102 | 103 | public void UseDefault() 104 | { 105 | Colors = new RGBAColor[96]; 106 | Array.Copy(Global.DefaultColors, Colors, 96); 107 | Global.KeyColors = Colors; 108 | } 109 | 110 | public CustomColor Shuffle() 111 | { 112 | CustomColor shuffled = new() 113 | { 114 | Colors = new RGBAColor[Colors.Length] 115 | }; 116 | Array.Copy(Colors, shuffled.Colors, Colors.Length); 117 | Random rand = new Random(DateTime.Now.Millisecond); 118 | for (int i = 0; i < shuffled.Colors.Length; i++) 119 | { 120 | int x, y; 121 | RGBAColor col; 122 | x = rand.Next(0, shuffled.Colors.Length); 123 | do 124 | { 125 | y = rand.Next(0, shuffled.Colors.Length); 126 | } while (y == x); 127 | 128 | col = shuffled.Colors[x]; 129 | shuffled.Colors[x] = shuffled.Colors[y]; 130 | shuffled.Colors[y] = col; 131 | } 132 | 133 | return shuffled; 134 | } 135 | 136 | public CustomColor Exchange(RGBAColor[] colors) 137 | { 138 | CustomColor old = new() 139 | { 140 | Colors = new RGBAColor[Colors.Length] 141 | }; 142 | Array.Copy(Colors, old.Colors, Colors.Length); 143 | Colors = new RGBAColor[colors.Length]; 144 | colors.CopyTo(Colors, 0); 145 | return old; 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /UI/RippleEffectDecorator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | using System.Windows.Controls; 9 | using System.Windows.Data; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Animation; 13 | using System.Windows.Shapes; 14 | using System.Runtime.CompilerServices; 15 | 16 | namespace QQS_UI.UI 17 | { 18 | public class RippleEffectDecorator : ContentControl 19 | { 20 | new public double Opacity 21 | { 22 | get { return (double)GetValue(OpacityProperty); } 23 | set { SetValue(OpacityProperty, value); } 24 | } 25 | 26 | // Using a DependencyProperty as the backing store for Opacity. This enables animation, styling, binding, etc... 27 | new public static readonly DependencyProperty OpacityProperty = 28 | DependencyProperty.Register("Opacity", typeof(double), typeof(RippleEffectDecorator), new PropertyMetadata(0.4)); 29 | 30 | public double ExpandTime 31 | { 32 | get { return (double)GetValue(ExpandTimeProperty); } 33 | set { SetValue(ExpandTimeProperty, value); } 34 | } 35 | 36 | // Using a DependencyProperty as the backing store for ExpandTime. This enables animation, styling, binding, etc... 37 | public static readonly DependencyProperty ExpandTimeProperty = 38 | DependencyProperty.Register("ExpandTime", typeof(double), typeof(RippleEffectDecorator), new PropertyMetadata(0.4)); 39 | 40 | public double FadeTime 41 | { 42 | get { return (double)GetValue(FadeTimeProperty); } 43 | set { SetValue(FadeTimeProperty, value); } 44 | } 45 | 46 | // Using a DependencyProperty as the backing store for FadeTime. This enables animation, styling, binding, etc... 47 | public static readonly DependencyProperty FadeTimeProperty = 48 | DependencyProperty.Register("FadeTime", typeof(double), typeof(RippleEffectDecorator), new PropertyMetadata(0.3)); 49 | 50 | public RippleEffectDecorator() 51 | { 52 | } 53 | 54 | public override void OnApplyTemplate() 55 | { 56 | base.OnApplyTemplate(); 57 | 58 | var parentGrid = new Grid(); 59 | var grid = new Grid(); 60 | var content = new ContentControl(); 61 | grid.Background = Brushes.Transparent; 62 | grid.ClipToBounds = true; 63 | parentGrid.Children.Add(grid); 64 | parentGrid.Children.Add(content); 65 | 66 | var c = Content; 67 | this.Content = parentGrid; 68 | content.Content = c; 69 | 70 | grid.SetBinding(WidthProperty, new Binding("ActualWidth") { Source = parentGrid }); 71 | grid.SetBinding(HeightProperty, new Binding("ActualHeight") { Source = parentGrid }); 72 | 73 | parentGrid.PreviewMouseDown += (sender, e) => 74 | { 75 | var targetWidth = (Math.Max(ActualWidth, ActualHeight) * 2) / ExpandTime * (ExpandTime + FadeTime); 76 | var mousePosition = e.GetPosition(this); 77 | var startMargin = new Thickness(mousePosition.X, mousePosition.Y, 0, 0); 78 | var endMargin = new Thickness(mousePosition.X - targetWidth / 2, mousePosition.Y - targetWidth / 2, 0, 0); 79 | 80 | var ellipse = new Ellipse() 81 | { 82 | Fill = Brushes.White, 83 | HorizontalAlignment = HorizontalAlignment.Left, 84 | VerticalAlignment = VerticalAlignment.Top, 85 | Opacity = Opacity 86 | }; 87 | ellipse.Margin = startMargin; 88 | ellipse.SetBinding(HeightProperty, new Binding("Width") { Source = ellipse }); 89 | 90 | Storyboard storyboard = new Storyboard(); 91 | 92 | var expand = new DoubleAnimation(0, targetWidth, new Duration(TimeSpan.FromSeconds(ExpandTime + FadeTime))); 93 | storyboard.Children.Add(expand); 94 | Storyboard.SetTarget(expand, ellipse); 95 | Storyboard.SetTargetProperty(expand, new PropertyPath(WidthProperty)); 96 | 97 | var marginShrink = new ThicknessAnimation(startMargin, endMargin, new Duration(TimeSpan.FromSeconds(ExpandTime + FadeTime))); 98 | storyboard.Children.Add(marginShrink); 99 | Storyboard.SetTarget(marginShrink, ellipse); 100 | Storyboard.SetTargetProperty(marginShrink, new PropertyPath(MarginProperty)); 101 | 102 | var opacity = new DoubleAnimation(Opacity, 0, new Duration(TimeSpan.FromSeconds(FadeTime))) 103 | { 104 | BeginTime = TimeSpan.FromSeconds(ExpandTime) 105 | }; 106 | storyboard.Children.Add(opacity); 107 | Storyboard.SetTarget(opacity, ellipse); 108 | Storyboard.SetTargetProperty(opacity, new PropertyPath(Ellipse.OpacityProperty)); 109 | 110 | grid.Children.Add(ellipse); 111 | 112 | storyboard.Begin(); 113 | 114 | var waitTime = ExpandTime + FadeTime; 115 | Task.Run(() => 116 | { 117 | Thread.Sleep(TimeSpan.FromSeconds(waitTime)); 118 | Dispatcher.Invoke(() => 119 | { 120 | grid.Children.Remove(ellipse); 121 | }); 122 | }); 123 | e.Handled = false; 124 | }; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /UI/BetterRadio.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | using System.Windows.Controls; 9 | using System.Windows.Data; 10 | using System.Windows.Documents; 11 | using System.Windows.Input; 12 | using System.Windows.Media; 13 | using System.Windows.Media.Animation; 14 | using System.Windows.Media.Imaging; 15 | using System.Windows.Navigation; 16 | using System.Windows.Shapes; 17 | 18 | namespace QQS_UI.UI 19 | { 20 | /// 21 | /// Interaction logic for BetterRadio.xaml 22 | /// 23 | public partial class BetterRadio : UserControl 24 | { 25 | public bool IsChecked 26 | { 27 | get { return (bool)GetValue(IsCheckedProperty); } 28 | set { SetValue(IsCheckedProperty, value); } 29 | } 30 | 31 | public static readonly DependencyProperty IsCheckedProperty = 32 | DependencyProperty.Register("IsChecked", typeof(bool), typeof(BetterRadio), new PropertyMetadata(false, (s, e) => ((BetterRadio)s).OnCheckChanged())); 33 | 34 | 35 | public string Text 36 | { 37 | get => (string)GetValue(TextProperty); 38 | set => SetValue(TextProperty, value); 39 | } 40 | 41 | public static readonly DependencyProperty TextProperty = 42 | DependencyProperty.Register("Text", typeof(string), typeof(BetterRadio), new PropertyMetadata("")); 43 | 44 | 45 | public int ParentDepth 46 | { 47 | get { return (int)GetValue(ParentDepthProperty); } 48 | set { SetValue(ParentDepthProperty, value); } 49 | } 50 | 51 | public static readonly DependencyProperty ParentDepthProperty = 52 | DependencyProperty.Register("ParentDepth", typeof(int), typeof(BetterRadio), new PropertyMetadata(1)); 53 | 54 | 55 | public static readonly RoutedEvent RadioCheckedEvent = EventManager.RegisterRoutedEvent( 56 | "RadioChecked", RoutingStrategy.Bubble, 57 | typeof(RoutedEventHandler), typeof(BetterRadio)); 58 | 59 | public event RoutedEventHandler RadioChecked 60 | { 61 | add { AddHandler(RadioCheckedEvent, value); } 62 | remove { RemoveHandler(RadioCheckedEvent, value); } 63 | } 64 | 65 | 66 | void OnCheckChanged() 67 | { 68 | if (IsChecked) 69 | { 70 | if (ParentDepth != 0) 71 | { 72 | FrameworkElement p = (FrameworkElement)Parent; 73 | for (int i = 1; i < ParentDepth; i++) 74 | { 75 | p = (FrameworkElement)p.Parent; 76 | } 77 | RecursiveUncheck(p); 78 | } 79 | RaiseEvent(new RoutedEventArgs(RadioCheckedEvent)); 80 | } 81 | } 82 | 83 | 84 | public BetterRadio() 85 | { 86 | InitializeComponent(); 87 | new InplaceConverter(new[] { 88 | new BBinding(IsCheckedProperty, this) 89 | }, v => (bool)v[0] ? Visibility.Visible : Visibility.Hidden) 90 | .Set(checkedBox, VisibilityProperty); 91 | } 92 | 93 | private void RecursiveUncheck(FrameworkElement p) 94 | { 95 | if (p is Panel panel) 96 | { 97 | foreach (object c in panel.Children) 98 | { 99 | if (c is FrameworkElement element) 100 | { 101 | RecursiveUncheck(element); 102 | } 103 | } 104 | } 105 | 106 | if (p is BetterRadio radio && p != this) 107 | { 108 | radio.IsChecked = false; 109 | } 110 | } 111 | 112 | private void DockPanel_MouseDown(object sender, MouseButtonEventArgs e) 113 | { 114 | IsChecked = true; 115 | 116 | double ExpandTime = 0.1; 117 | double FadeTime = 0.1; 118 | 119 | double o = 0.7; 120 | 121 | double targetWidth = rippleBox.ActualWidth; 122 | 123 | Ellipse ellipse = new Ellipse() 124 | { 125 | Fill = (Brush)Resources["PrimaryBrush"], 126 | HorizontalAlignment = HorizontalAlignment.Center, 127 | VerticalAlignment = VerticalAlignment.Center, 128 | Opacity = o 129 | }; 130 | _ = ellipse.SetBinding(HeightProperty, new Binding("Width") { Source = ellipse }); 131 | 132 | Storyboard storyboard = new Storyboard(); 133 | 134 | DoubleAnimation expand = new DoubleAnimation(10, targetWidth, new Duration(TimeSpan.FromSeconds(ExpandTime + FadeTime))); 135 | storyboard.Children.Add(expand); 136 | Storyboard.SetTarget(expand, ellipse); 137 | Storyboard.SetTargetProperty(expand, new PropertyPath(WidthProperty)); 138 | 139 | DoubleAnimation opacity = new DoubleAnimation(o, 0, new Duration(TimeSpan.FromSeconds(FadeTime))) 140 | { 141 | BeginTime = TimeSpan.FromSeconds(ExpandTime) 142 | }; 143 | storyboard.Children.Add(opacity); 144 | Storyboard.SetTarget(opacity, ellipse); 145 | Storyboard.SetTargetProperty(opacity, new PropertyPath(Ellipse.OpacityProperty)); 146 | 147 | rippleBox.Children.Add(ellipse); 148 | 149 | storyboard.Begin(); 150 | 151 | double waitTime = ExpandTime + FadeTime; 152 | _ = Task.Run(() => 153 | { 154 | Thread.Sleep(TimeSpan.FromSeconds(waitTime)); 155 | Dispatcher.Invoke(() => 156 | { 157 | rippleBox.Children.Remove(ellipse); 158 | }); 159 | }); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /UI/ValueSlider.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Navigation; 14 | using System.Windows.Shapes; 15 | 16 | namespace QQS_UI.UI 17 | { 18 | /// 19 | /// Interaction logic for ValueSlider.xaml 20 | /// 21 | public partial class ValueSlider : UserControl 22 | { 23 | public double Minimum 24 | { 25 | get { return (double)GetValue(MinimumProperty); } 26 | set { SetValue(MinimumProperty, value); } 27 | } 28 | 29 | public static readonly DependencyProperty MinimumProperty = 30 | DependencyProperty.Register("Minimum", typeof(double), typeof(ValueSlider), new PropertyMetadata(0.0)); 31 | 32 | 33 | public double Maximum 34 | { 35 | get { return (double)GetValue(MaximumProperty); } 36 | set { SetValue(MaximumProperty, value); } 37 | } 38 | 39 | public static readonly DependencyProperty MaximumProperty = 40 | DependencyProperty.Register("Maximum", typeof(double), typeof(ValueSlider), new PropertyMetadata(1.0)); 41 | 42 | 43 | public double Value 44 | { 45 | get { return (double)GetValue(ValueProperty); } 46 | set { SetValue(ValueProperty, value); } 47 | } 48 | 49 | public static readonly DependencyProperty ValueProperty = 50 | DependencyProperty.Register("Value", typeof(double), typeof(ValueSlider), new PropertyMetadata(0.0, (s, e) => (s as ValueSlider).OnValueChange(e))); 51 | 52 | 53 | public int DecimalPoints 54 | { 55 | get { return (int)GetValue(DecimalPointsProperty); } 56 | set { SetValue(DecimalPointsProperty, value); } 57 | } 58 | 59 | public static readonly DependencyProperty DecimalPointsProperty = 60 | DependencyProperty.Register("DecimalPoints", typeof(int), typeof(ValueSlider), new PropertyMetadata(0)); 61 | 62 | 63 | public decimal TrueMin 64 | { 65 | get { return (decimal)GetValue(TrueMinProperty); } 66 | set { SetValue(TrueMinProperty, value); } 67 | } 68 | 69 | public static readonly DependencyProperty TrueMinProperty = 70 | DependencyProperty.Register("TrueMin", typeof(decimal), typeof(ValueSlider), new PropertyMetadata((decimal)0.0)); 71 | 72 | 73 | public decimal TrueMax 74 | { 75 | get { return (decimal)GetValue(TrueMaxProperty); } 76 | set { SetValue(TrueMaxProperty, value); } 77 | } 78 | 79 | public static readonly DependencyProperty TrueMaxProperty = 80 | DependencyProperty.Register("TrueMax", typeof(decimal), typeof(ValueSlider), new PropertyMetadata((decimal)1.0d)); 81 | 82 | public decimal Step 83 | { get => (decimal)GetValue(StepProperty); set => SetValue(StepProperty, value); } 84 | public static readonly DependencyProperty StepProperty = DependencyProperty.Register("Step", typeof(decimal), typeof(ValueSlider), new PropertyMetadata((decimal)1)); 85 | 86 | 87 | public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent( 88 | "ValueChanged", RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler), typeof(ValueSlider)); 89 | 90 | public event RoutedPropertyChangedEventHandler ValueChanged 91 | { 92 | add { AddHandler(ValueChangedEvent, value); } 93 | remove { RemoveHandler(ValueChangedEvent, value); } 94 | } 95 | 96 | void OnValueChange(DependencyPropertyChangedEventArgs e) 97 | { 98 | if (!ignoreslider) slider.Value = nudToSlider(Value); 99 | if (!ignorevalue) updown.Value = (decimal)Value; 100 | ignoreslider = false; 101 | ignorevalue = false; 102 | RaiseEvent(new RoutedPropertyChangedEventArgs((double)e.OldValue, (double)e.NewValue, ValueChangedEvent)); 103 | } 104 | 105 | public Func sliderToNud = v => v; 106 | public Func nudToSlider = v => v; 107 | 108 | public ValueSlider() 109 | { 110 | InitializeComponent(); 111 | 112 | FocusVisualStyle = null; 113 | 114 | slider.SetBinding(BetterSlider.MaximumProperty, new Binding("Maximum") { Source = this }); 115 | slider.SetBinding(BetterSlider.MinimumProperty, new Binding("Minimum") { Source = this }); 116 | updown.SetBinding(NumberSelect.MinimumProperty, new Binding("TrueMin") { Source = this }); 117 | updown.SetBinding(NumberSelect.MaximumProperty, new Binding("TrueMax") { Source = this }); 118 | updown.SetBinding(NumberSelect.DecimalPointsProperty, new Binding("DecimalPoints") { Source = this }); 119 | updown.SetBinding(NumberSelect.StepProperty, new Binding("Step") { Source = this }); 120 | } 121 | 122 | bool ignoreslider = false; 123 | bool ignorevalue = false; 124 | 125 | private void Slider_ValueChanged(object sender, double e) 126 | { 127 | if (IsInitialized) 128 | { 129 | ignoreslider = true; 130 | Value = sliderToNud(slider.Value); 131 | } 132 | } 133 | 134 | private void Updown_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) 135 | { 136 | if (IsInitialized) 137 | { 138 | if (!ignorevalue) 139 | { 140 | ignorevalue = true; 141 | Value = (double)updown.Value; 142 | } 143 | ignorevalue = false; 144 | } 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Core/Global.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.CompilerServices; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using SharpExtension.Collections; 8 | 9 | namespace QQS_UI.Core 10 | { 11 | /// 12 | /// 存放全局数据. 13 | /// 14 | public static class Global 15 | { 16 | public static readonly short[] GenKeyX = { 17 | 0, 12, 18, 33, 36, 54, 66, 72, 85, 90, 105, 108 18 | }; 19 | public static readonly short[] DrawMap = { 20 | 0, 2, 4, 5, 7, 9, 11, 12, 14, 16, 17, 19, 21, 23, 24, 26, 28, 29, 21 | 31, 33, 35, 36, 38, 40, 41, 43, 45, 47, 48, 50, 52, 53, 55, 57, 22 | 59, 60, 62, 64, 65, 67, 69, 71, 72, 74, 76, 77, 79, 81, 83, 84, 23 | 86, 88, 89, 91, 93, 95, 96, 98, 100, 101, 103, 105, 107, 108, 24 | 110, 112, 113, 115, 117, 119, 120, 122, 124, 125, 127, 1, 3, 25 | 6, 8, 10, 13, 15, 18, 20, 22, 25, 27, 30, 32, 34, 37, 39, 42, 44, 26 | 46, 49, 51, 54, 56, 58, 61, 63, 66, 68, 70, 73, 75, 78, 80, 82, 27 | 85, 87, 90, 92, 94, 97, 99, 102, 104, 106, 109, 111, 114, 116, 28 | 118, 121, 123, 126 29 | }; 30 | public static readonly RGBAColor[] DefaultColors = { 31 | 0xFF3366FF, 0xFFFF7E33, 0xFF33FF66, 0xFFFF3381, 0xFF33E1E1, 0xFFE433E1, 32 | 0xFF99E133, 0xFF4B33E1, 0xFFFFCC33, 0xFF33B4FF, 0xFFFF3333, 0xFF33FFB1, 33 | 0xFFFF33CC, 0xFF4EFF33, 0xFF9933FF, 0xFFE7FF33, 0xFF3366FF, 0xFFFF7E33, 34 | 0xFF33FF66, 0xFFFF3381, 0xFF33E1E1, 0xFFE433E1, 0xFF99E133, 0xFF4B33E1, 35 | 0xFFFFCC33, 0xFF33B4FF, 0xFFFF3333, 0xFF33FFB1, 0xFFFF33CC, 0xFF4EFF33, 36 | 0xFF9933FF, 0xFFE7FF33, 0xFF3366FF, 0xFFFF7E33, 0xFF33FF66, 0xFFFF3381, 37 | 0xFF33E1E1, 0xFFE433E1, 0xFF99E133, 0xFF4B33E1, 0xFFFFCC33, 0xFF33B4FF, 38 | 0xFFFF3333, 0xFF33FFB1, 0xFFFF33CC, 0xFF4EFF33, 0xFF9933FF, 0xFFE7FF33, 39 | 0xFF3366FF, 0xFFFF7E33, 0xFF33FF66, 0xFFFF3381, 0xFF33E1E1, 0xFFE433E1, 40 | 0xFF99E133, 0xFF4B33E1, 0xFFFFCC33, 0xFF33B4FF, 0xFFFF3333, 0xFF33FFB1, 41 | 0xFFFF33CC, 0xFF4EFF33, 0xFF9933FF, 0xFFE7FF33, 0xFF3366FF, 0xFFFF7E33, 42 | 0xFF33FF66, 0xFFFF3381, 0xFF33E1E1, 0xFFE433E1, 0xFF99E133, 0xFF4B33E1, 43 | 0xFFFFCC33, 0xFF33B4FF, 0xFFFF3333, 0xFF33FFB1, 0xFFFF33CC, 0xFF4EFF33, 44 | 0xFF9933FF, 0xFFE7FF33, 0xFF3366FF, 0xFFFF7E33, 0xFF33FF66, 0xFFFF3381, 45 | 0xFF33E1E1, 0xFFE433E1, 0xFF99E133, 0xFF4B33E1, 0xFFFFCC33, 0xFF33B4FF, 46 | 0xFFFF3333, 0xFF33FFB1, 0xFFFF33CC, 0xFF4EFF33, 0xFF9933FF, 0xFFE7FF33 47 | }; 48 | /// 49 | /// 渲染器对象实际使用的键盘颜色.
50 | /// Key colors that renderer actually uses. 51 | ///
52 | public static RGBAColor[] KeyColors; 53 | /// 54 | /// 渲染器对象实际使用的音符颜色.
55 | /// Note colors that renderer actually uses. 56 | ///
57 | public static RGBAColor[] NoteColors; 58 | static Global() 59 | { 60 | KeyColors = new RGBAColor[96]; 61 | NoteColors = new RGBAColor[96]; 62 | Array.Copy(DefaultColors, KeyColors, 96); 63 | Array.Copy(DefaultColors, NoteColors, 96); 64 | } 65 | /// 66 | /// 将 Midi 时间转换为 .
67 | /// Converts midi time to a new instance. 68 | ///
69 | public static TimeSpan GetTimeOf(uint midiTime, ushort ppq, UnmanagedList tempos) 70 | { 71 | if (tempos == null) 72 | { 73 | return new TimeSpan((long)(5000000.0 * midiTime / ppq)); 74 | } 75 | if (tempos.Count == 0) 76 | { 77 | return new TimeSpan((long)(5000000.0 * midiTime / ppq)); 78 | } 79 | double ticks = 0; 80 | double tempo = 500000.0; 81 | uint lastEventTime = 0; 82 | IIterator iterator = tempos.GetIterator(); 83 | while (iterator.MoveNext()) 84 | { 85 | if (iterator.Current.Tick > midiTime) 86 | { 87 | break; 88 | } 89 | uint dtTime = iterator.Current.Tick - lastEventTime; 90 | ticks += tempo * 10.0 * dtTime / ppq; 91 | lastEventTime = iterator.Current.Tick; 92 | tempo = iterator.Current.Value; 93 | } 94 | ticks += tempo * 10.0 * (midiTime - lastEventTime) / ppq; 95 | return new TimeSpan((long)ticks); 96 | } 97 | 98 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 99 | public static unsafe uint ParseVLInt(ref byte* p) 100 | { 101 | uint result = 0; 102 | uint b; 103 | do 104 | { 105 | b = *p++; 106 | result = (result << 7) | (b & 0x7F); 107 | } 108 | while ((b & 0b10000000) != 0); 109 | return result; 110 | } 111 | 112 | /// 113 | /// 表示预览时渲染FPS是否被控制不高于目标FPS.
114 | /// Determines whether render FPS cannot be greater than target FPS. 115 | ///
116 | public static bool LimitPreviewFPS = true; 117 | public static bool PreviewPaused = false; 118 | 119 | public static double PressedWhiteKeyGradientScale = 1.0025; 120 | public const double DefaultPressedWhiteKeyGradientScale = 1.0025; 121 | 122 | public static double NoteGradientScale = 1.08; 123 | public const double DefaultNoteGradientScale = 1.08; 124 | 125 | public static double UnpressedWhiteKeyGradientScale = 1.002; 126 | public const double DefaultUnpressedWhiteKeyGradientScale = 1.002; 127 | 128 | public static double SeparatorGradientScale = 1.08; 129 | public const double DefaultSeparatorGradientScale = 1.08; 130 | 131 | public static int MaxMIDILoaderConcurrency = -1; 132 | public static int MaxRenderConcurrency = -1; 133 | 134 | public static bool EnableNoteBorder = true; 135 | public static double NoteBorderWidth = 1; 136 | public static bool EnableDenseNoteEffect = true; 137 | public static double NoteBorderShade = 5; 138 | public static double DenseNoteShade = 5; 139 | 140 | public static bool TranslucentNotes = false; 141 | public static byte NoteAlpha = 255; 142 | } 143 | 144 | public struct PreviewEvent 145 | { 146 | public uint Time; 147 | public uint Value; 148 | } 149 | 150 | public enum HorizontalGradientDirection 151 | { 152 | FromLeftToRight, 153 | FromRightToLeft 154 | } 155 | public enum VerticalGradientDirection 156 | { 157 | FromButtomToTop, 158 | FromTopToButtom 159 | } 160 | public enum VideoQualityOptions 161 | { 162 | CRF, 163 | Bitrate 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /UI/Scrollbar.xaml: -------------------------------------------------------------------------------- 1 | 4 | 15 | 45 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /Core/NoteSorter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.CompilerServices; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using SharpExtension; 8 | using SharpExtension.Collections; 9 | 10 | namespace QQS_UI.Core 11 | { 12 | /// 13 | /// 对音符序列进行排序. 这是.NET BCL中Array.Sort对于的特化版本. 14 | /// 15 | public static unsafe class NoteSorter 16 | { 17 | // 实现: https://referencesource.microsoft.com/#q=GenericArraySortHelper.IntroSort" 18 | 19 | /// 20 | /// 获取递归的深度. 21 | /// 22 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 23 | private static long GetRecursionDepth(long len) 24 | { 25 | long res = 0; 26 | while (len >= 1) 27 | { 28 | ++res; 29 | len /= 2; 30 | } 31 | return res; 32 | } 33 | /// 34 | /// 如果 a处元素 > b处元素, 进行交换 35 | /// 36 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 37 | private static void SwapIfGreater(in Note* collection, in long a, in long b) 38 | { 39 | if (a != b) 40 | { 41 | if (!Compare(collection[a], collection[b])) 42 | { 43 | Note n = collection[a]; 44 | collection[a] = collection[b]; 45 | collection[b] = n; 46 | } 47 | } 48 | } 49 | /// 50 | /// 对前后 Note 进行比较. 51 | /// 52 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 53 | public static bool Compare(in Note left, in Note right) 54 | { 55 | // 如果符合以下条件 (返回true), 那么 left < right, 也就是 left 应该排在 right 的前面. 56 | return left.Start < right.Start || (left.Track < right.Track && left.Start == right.Start); 57 | } 58 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 59 | private static void InsertionSort(in Note* collection, in long low, in long high) 60 | { 61 | long i, j; 62 | Note t; 63 | for (i = low; i != high; ++i) 64 | { 65 | j = i; 66 | t = collection[i + 1]; 67 | 68 | while (j >= low && Compare(t, collection[j])) 69 | { 70 | collection[j + 1] = collection[j]; 71 | --j; 72 | } 73 | 74 | collection[j + 1] = t; 75 | } 76 | } 77 | /// 78 | /// 内省排序. 79 | /// 80 | [MethodImpl(MethodImplOptions.AggressiveOptimization)] 81 | private static void IntroSort(in Note* collection, in long low, long high, long depth) 82 | { 83 | while (high > low) 84 | { 85 | long partitionSize = high - low + 1; 86 | if (partitionSize < 16) 87 | { 88 | if (partitionSize == 1) 89 | { 90 | return; 91 | } 92 | if (partitionSize == 2) 93 | { 94 | SwapIfGreater(collection, low, high); 95 | } 96 | if (partitionSize == 3) 97 | { 98 | SwapIfGreater(collection, low, high - 1); 99 | SwapIfGreater(collection, low, high); 100 | SwapIfGreater(collection, high - 1, high); 101 | return; 102 | } 103 | InsertionSort(collection, low, high); 104 | } 105 | if (depth == 0) 106 | { 107 | HeapSort(collection, low, high); 108 | return; 109 | } 110 | --depth; 111 | 112 | long p = PickPivotAndPartition(collection, low, high); 113 | IntroSort(collection, p + 1, high, depth); 114 | high = p - 1; 115 | } 116 | } 117 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 118 | private static void DownHeap(in Note* collection, long i, in long n, in long low) 119 | { 120 | Note d = collection[low + i - 1]; 121 | long child; 122 | while (i <= n / 2) 123 | { 124 | child = 2 * i; 125 | if (child < n && Compare(collection[low + child - 1], collection[low + child])) 126 | { 127 | child++; 128 | } 129 | if (!Compare(d, collection[low + child - 1])) 130 | { 131 | break; 132 | } 133 | 134 | collection[low + i - 1] = collection[low + child - 1]; 135 | i = child; 136 | } 137 | collection[low + i - 1] = d; 138 | } 139 | /// 140 | /// 堆排序. 141 | /// 142 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 143 | private static void HeapSort(in Note* collection, in long low, in long high) 144 | { 145 | long n = high - low + 1; 146 | for (long i = n / 2; i >= 1; --i) 147 | { 148 | DownHeap(collection, i, n, low); 149 | } 150 | for (long i = n; i > 1; --i) 151 | { 152 | //Swap(lo, lo + i - 1); 153 | Note note = collection[low]; 154 | collection[low] = collection[low + i - 1]; 155 | collection[low + i - 1] = note; 156 | 157 | DownHeap(collection, 1, i - 1, low); 158 | } 159 | } 160 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 161 | public static void Sort(UnmanagedList collection) 162 | { 163 | if (collection.Count < 2) 164 | { 165 | return; 166 | } 167 | lock (collection) 168 | { 169 | IntroSort((Note*)UnsafeMemory.GetActualAddressOf(ref collection[0]), 0, collection.Count - 1, collection.Count); 170 | } 171 | } 172 | 173 | private static long PickPivotAndPartition(in Note* collection, in long lo, in long hi) 174 | { 175 | long mid = lo + (hi - lo) / 2; 176 | SwapIfGreater(collection, lo, mid); 177 | SwapIfGreater(collection, lo, hi); 178 | SwapIfGreater(collection, mid, hi); 179 | 180 | Note pivot = collection[mid]; 181 | //Swap(mid, hi - 1); 182 | Note m = collection[mid]; 183 | collection[mid] = collection[hi - 1]; 184 | collection[hi - 1] = m; 185 | 186 | long left = lo, right = hi - 1; 187 | 188 | while (left < right) 189 | { 190 | while (Compare(collection[++left], pivot)) 191 | { 192 | 193 | } 194 | 195 | while (Compare(pivot, collection[--right])) 196 | { 197 | 198 | } 199 | 200 | if (left >= right) 201 | { 202 | break; 203 | } 204 | 205 | m = collection[left]; 206 | collection[left] = collection[right]; 207 | collection[right] = m; 208 | } 209 | 210 | m = collection[left]; 211 | collection[left] = collection[hi - 1]; 212 | collection[hi - 1] = m; 213 | return left; 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /UI/NumberSelect.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Navigation; 14 | using System.Windows.Shapes; 15 | 16 | namespace QQS_UI.UI 17 | { 18 | /// 19 | /// Interaction logic for NumberSelect.xaml 20 | /// 21 | public partial class NumberSelect : UserControl 22 | { 23 | public decimal Value 24 | { get => (decimal)GetValue(ValueProperty); set => SetValue(ValueProperty, value); } 25 | public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(decimal), typeof(NumberSelect), new PropertyMetadata((decimal)0, new PropertyChangedCallback(OnPropertyChange))); 26 | public int DecimalPoints 27 | { get => (int)GetValue(DecimalPointsProperty); set => SetValue(DecimalPointsProperty, value); } 28 | public static readonly DependencyProperty DecimalPointsProperty = DependencyProperty.Register("DecimalPoints", typeof(int), typeof(NumberSelect), new PropertyMetadata((int)0, new PropertyChangedCallback(OnPropertyChange))); 29 | public decimal Minimum 30 | { get => (decimal)GetValue(MinimumProperty); set => SetValue(MinimumProperty, value); } 31 | public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register("Minimum", typeof(decimal), typeof(NumberSelect), new PropertyMetadata((decimal)0, new PropertyChangedCallback(OnPropertyChange))); 32 | public decimal Maximum 33 | { get => (decimal)GetValue(MaximumProperty); set => SetValue(MaximumProperty, value); } 34 | public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register("Maximum", typeof(decimal), typeof(NumberSelect), new PropertyMetadata((decimal)1000, new PropertyChangedCallback(OnPropertyChange))); 35 | public decimal Step 36 | { get => (decimal)GetValue(StepProperty); set => SetValue(StepProperty, value); } 37 | public static readonly DependencyProperty StepProperty = DependencyProperty.Register("Step", typeof(decimal), typeof(NumberSelect), new PropertyMetadata((decimal)1)); 38 | 39 | public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent( 40 | "ValueChanged", RoutingStrategy.Bubble, 41 | typeof(RoutedPropertyChangedEventHandler), typeof(NumberSelect)); 42 | 43 | public event RoutedPropertyChangedEventHandler ValueChanged 44 | { 45 | add { AddHandler(ValueChangedEvent, value); } 46 | remove { RemoveHandler(ValueChangedEvent, value); } 47 | } 48 | 49 | private static void OnPropertyChange(DependencyObject sender, DependencyPropertyChangedEventArgs e) 50 | { 51 | ((NumberSelect)sender).UpdateValue(); 52 | } 53 | 54 | public bool TextFocused => textBox.IsFocused; 55 | 56 | string prevText = ""; 57 | 58 | public NumberSelect() 59 | { 60 | InitializeComponent(); 61 | 62 | prevText = Value.ToString(); 63 | textBox.Text = prevText; 64 | 65 | upArrow.SetBinding(IsEnabledProperty, new BBinding(IsEnabledProperty, this)); 66 | downArrow.SetBinding(IsEnabledProperty, new BBinding(IsEnabledProperty, this)); 67 | textBox.SetBinding(IsEnabledProperty, new BBinding(IsEnabledProperty, this)); 68 | } 69 | 70 | bool ignoreChange = false; 71 | void UpdateValue() 72 | { 73 | if (!ignoreChange) 74 | { 75 | try 76 | { 77 | decimal old = Value; 78 | decimal d = Decimal.Round(old, DecimalPoints); 79 | if (d < Minimum) d = Minimum; 80 | if (d > Maximum) d = Maximum; 81 | if (d != old) 82 | { 83 | Value = d; 84 | } 85 | try 86 | { 87 | RaiseEvent(new RoutedPropertyChangedEventArgs(old, d, ValueChangedEvent)); 88 | } 89 | catch { } 90 | } 91 | catch { } 92 | textBox.Text = Value.ToString(); 93 | } 94 | ignoreChange = false; 95 | } 96 | 97 | private void TextBox_TextChanged(object sender, TextChangedEventArgs e) 98 | { 99 | try 100 | { 101 | decimal _d = Convert.ToDecimal(textBox.Text); 102 | decimal d = Decimal.Round(_d, DecimalPoints); 103 | if (d < Minimum) d = Minimum; 104 | if (d > Maximum) d = Maximum; 105 | else 106 | { 107 | var old = Value; 108 | if (d != old) 109 | { 110 | ignoreChange = true; 111 | Value = d; 112 | try 113 | { 114 | RaiseEvent(new RoutedPropertyChangedEventArgs(old, d, ValueChangedEvent)); 115 | } 116 | catch { } 117 | } 118 | } 119 | } 120 | catch 121 | { 122 | if(textBox.Text != "") 123 | textBox.Text = prevText; 124 | } 125 | prevText = textBox.Text; 126 | } 127 | 128 | private void TextBox_LostFocus(object sender, RoutedEventArgs e) 129 | { 130 | CheckAndSave(); 131 | } 132 | 133 | void CheckAndSave() 134 | { 135 | try 136 | { 137 | decimal _d = Convert.ToDecimal(textBox.Text); 138 | decimal d = Decimal.Round(_d, DecimalPoints); 139 | if (d < Minimum) d = Minimum; 140 | if (d > Maximum) d = Maximum; 141 | var old = Value; 142 | if (d != old) 143 | { 144 | Value = d; 145 | try 146 | { 147 | RaiseEvent(new RoutedPropertyChangedEventArgs(old, d, ValueChangedEvent)); 148 | } 149 | catch { } 150 | } 151 | } 152 | catch { } 153 | textBox.Text = Value.ToString(); 154 | } 155 | 156 | private void TextBox_TextInput(object sender, TextCompositionEventArgs e) 157 | { 158 | 159 | } 160 | 161 | private void Button_Click(object sender, RoutedEventArgs e) 162 | { 163 | var d = Value + Step; 164 | if (d < Minimum) d = Minimum; 165 | if (d > Maximum) d = Maximum; 166 | var old = Value; 167 | Value = d; 168 | textBox.Text = Value.ToString(); 169 | if (old != d) 170 | RaiseEvent(new RoutedPropertyChangedEventArgs(old, d, ValueChangedEvent)); 171 | } 172 | 173 | private void Button_Click_1(object sender, RoutedEventArgs e) 174 | { 175 | var d = Value - Step; 176 | if (d < Minimum) d = Minimum; 177 | if (d > Maximum) d = Maximum; 178 | var old = Value; 179 | Value = d; 180 | textBox.Text = Value.ToString(); 181 | if (old != d) 182 | RaiseEvent(new RoutedPropertyChangedEventArgs(old, d, ValueChangedEvent)); 183 | } 184 | 185 | private void UserControl_KeyDown(object sender, KeyEventArgs e) 186 | { 187 | if(e.Key == Key.Enter) 188 | { 189 | CheckAndSave(); 190 | e.Handled = true; 191 | Keyboard.ClearFocus(); 192 | 193 | FrameworkElement parent = (FrameworkElement)textBox.Parent; 194 | while (parent != null && parent is IInputElement && !((IInputElement)parent).Focusable) 195 | { 196 | parent = (FrameworkElement)parent.Parent; 197 | } 198 | 199 | DependencyObject scope = FocusManager.GetFocusScope(textBox); 200 | FocusManager.SetFocusedElement(scope, parent as IInputElement); 201 | } 202 | } 203 | 204 | private void TextBox_KeyDown(object sender, KeyEventArgs e) 205 | { 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /UI/BetterSlider.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using System.Windows; 9 | using System.Windows.Controls; 10 | using System.Windows.Data; 11 | using System.Windows.Documents; 12 | using System.Windows.Input; 13 | using System.Windows.Media; 14 | using System.Windows.Media.Animation; 15 | using System.Windows.Media.Imaging; 16 | using System.Windows.Navigation; 17 | using System.Windows.Shapes; 18 | 19 | namespace QQS_UI.UI 20 | { 21 | /// 22 | /// Interaction logic for BetterSlider.xaml 23 | /// 24 | public partial class BetterSlider : UserControl 25 | { 26 | public double Minimum 27 | { 28 | get { return (double)GetValue(MinimumProperty); } 29 | set { SetValue(MinimumProperty, value); } 30 | } 31 | 32 | public static readonly DependencyProperty MinimumProperty = 33 | DependencyProperty.Register("Minimum", typeof(double), typeof(BetterSlider), new PropertyMetadata(0.0)); 34 | 35 | 36 | public double Maximum 37 | { 38 | get { return (double)GetValue(MaximumProperty); } 39 | set { SetValue(MaximumProperty, value); } 40 | } 41 | 42 | public static readonly DependencyProperty MaximumProperty = 43 | DependencyProperty.Register("Maximum", typeof(double), typeof(BetterSlider), new PropertyMetadata(0.0)); 44 | 45 | 46 | static void ValueChangedCallback(DependencyObject s, DependencyPropertyChangedEventArgs e) 47 | { 48 | var slider = (BetterSlider)s; 49 | //slider.ScaledValue = (slider.Value - slider.Minimum) / slider.Maximum * slider.barGrid.ActualWidth; 50 | } 51 | 52 | public double Value 53 | { 54 | get { return (double)GetValue(ValueProperty); } 55 | set 56 | { 57 | var v = value; 58 | if (v > Maximum) v = Maximum; 59 | if (v < Minimum) v = Minimum; 60 | SetValue(ValueProperty, v); 61 | } 62 | } 63 | 64 | public static readonly DependencyProperty ValueProperty = 65 | DependencyProperty.Register("Value", typeof(double), typeof(BetterSlider), new PropertyMetadata(0.0, ValueChangedCallback)); 66 | 67 | 68 | double ScaledValue 69 | { 70 | get { return (double)GetValue(ScaledValueProperty); } 71 | set { SetValue(ScaledValueProperty, value); } 72 | } 73 | 74 | static readonly DependencyProperty ScaledValueProperty = 75 | DependencyProperty.Register("ScaledValue", typeof(double), typeof(BetterSlider), new PropertyMetadata(0.0)); 76 | 77 | 78 | public event EventHandler UserValueChanged; 79 | 80 | 81 | public BetterSlider() 82 | { 83 | InitializeComponent(); 84 | 85 | new InplaceConverter(new[] 86 | { 87 | new BBinding(ValueProperty, this), 88 | new BBinding(MinimumProperty, this), 89 | new BBinding(MaximumProperty, this), 90 | new BBinding(ActualWidthProperty, barGrid), 91 | }, 92 | (values) => 93 | { 94 | try 95 | { 96 | return ((double)values[0] - (double)values[1]) / ((double)values[2] - (double)values[1]) * (double)values[3]; 97 | } 98 | catch { return 0; } 99 | }) 100 | .Set(this, ScaledValueProperty); 101 | 102 | new InplaceConverter(new[] { new BBinding(ScaledValueProperty, this) }, 103 | (values) => new Thickness((double)values[0], 0, 0, 0)) 104 | .Set(headGrid, MarginProperty); 105 | } 106 | 107 | private void ClickerGrid_MouseEnter(object sender, MouseEventArgs e) 108 | { 109 | //hoverEllipse.Visibility = Visibility.Visible; 110 | } 111 | 112 | private void ClickerGrid_MouseLeave(object sender, MouseEventArgs e) 113 | { 114 | //hoverEllipse.Visibility = Visibility.Hidden; 115 | } 116 | 117 | private void ClickerGrid_MouseDown(object sender, MouseButtonEventArgs e) 118 | { 119 | clickerGrid.CaptureMouse(); 120 | Value = e.GetPosition(barGrid).X / barGrid.ActualWidth * (Maximum - Minimum) + Minimum; 121 | UserValueChanged?.Invoke(this, Value); 122 | AddRipple(); 123 | this.Focus(); 124 | } 125 | 126 | private void ClickerGrid_MouseMove(object sender, MouseEventArgs e) 127 | { 128 | if (clickerGrid.IsMouseCaptureWithin) 129 | { 130 | Value = e.GetPosition(barGrid).X / barGrid.ActualWidth * (Maximum - Minimum) + Minimum; 131 | UserValueChanged?.Invoke(this, Value); 132 | } 133 | } 134 | 135 | private void ClickerGrid_MouseUp(object sender, MouseButtonEventArgs e) 136 | { 137 | clickerGrid.ReleaseMouseCapture(); 138 | } 139 | 140 | void AddRipple() 141 | { 142 | double ExpandTime = 0.1; 143 | double FadeTime = 0.1; 144 | 145 | double o = 0.7; 146 | 147 | var targetWidth = auraGrid.ActualWidth; 148 | 149 | var ellipse = new Ellipse() 150 | { 151 | Fill = (Brush)Resources["PrimaryBrush"], 152 | HorizontalAlignment = HorizontalAlignment.Center, 153 | VerticalAlignment = VerticalAlignment.Center, 154 | Opacity = o 155 | }; 156 | ellipse.SetBinding(HeightProperty, new Binding("Width") { Source = ellipse }); 157 | 158 | Storyboard storyboard = new Storyboard(); 159 | 160 | var expand = new DoubleAnimation(0, targetWidth, new Duration(TimeSpan.FromSeconds(ExpandTime + FadeTime))); 161 | storyboard.Children.Add(expand); 162 | Storyboard.SetTarget(expand, ellipse); 163 | Storyboard.SetTargetProperty(expand, new PropertyPath(WidthProperty)); 164 | 165 | var opacity = new DoubleAnimation(o, 0, new Duration(TimeSpan.FromSeconds(FadeTime))); 166 | opacity.BeginTime = TimeSpan.FromSeconds(ExpandTime); 167 | storyboard.Children.Add(opacity); 168 | Storyboard.SetTarget(opacity, ellipse); 169 | Storyboard.SetTargetProperty(opacity, new PropertyPath(Ellipse.OpacityProperty)); 170 | 171 | auraGrid.Children.Add(ellipse); 172 | 173 | storyboard.Begin(); 174 | 175 | var waitTime = ExpandTime + FadeTime; 176 | Task.Run(() => 177 | { 178 | Thread.Sleep(TimeSpan.FromSeconds(waitTime)); 179 | Dispatcher.Invoke(() => 180 | { 181 | headGrid.Children.Remove(ellipse); 182 | }); 183 | }); 184 | } 185 | } 186 | 187 | public class DoubleMultiplyConverter : IMultiValueConverter 188 | { 189 | public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture) 190 | { 191 | double r = 1; 192 | foreach (var v in values) r *= (double)v; 193 | return r; 194 | } 195 | 196 | public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture) 197 | { 198 | throw new NotImplementedException(); 199 | } 200 | } 201 | 202 | public class ScaledValueConverter : IMultiValueConverter 203 | { 204 | public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture) 205 | { 206 | return ((double)values[0] - (double)values[1]) / ((double)values[2] - (double)values[1]) * (double)values[3]; 207 | } 208 | 209 | public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture) 210 | { 211 | throw new NotImplementedException(); 212 | } 213 | } 214 | 215 | public class ThicknessConverter : IValueConverter 216 | { 217 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 218 | { 219 | return new Thickness( 220 | (double)value, 221 | 0, 222 | 0, 223 | 0 224 | ); 225 | } 226 | 227 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 228 | { 229 | throw new NotImplementedException(); 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /Core/CanvasBase.cs: -------------------------------------------------------------------------------- 1 | using SharpExtension; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Runtime.CompilerServices; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace QQS_UI.Core 10 | { 11 | public abstract unsafe class CanvasBase : IDisposable 12 | { 13 | protected readonly int width; 14 | protected readonly int height; 15 | protected readonly int fps; 16 | protected readonly int crf; 17 | protected readonly int keyh; 18 | protected readonly uint lineColor; 19 | protected readonly ulong frameSize; 20 | protected readonly RGBAColor background; 21 | protected FFMpeg pipe; 22 | protected readonly int[] keyx = new int[128]; 23 | protected readonly int[] notex = new int[128]; 24 | protected readonly int[] keyw = new int[128]; 25 | protected readonly int[] notew = new int[128]; 26 | /// 27 | /// 表示一个空帧的全部像素.
28 | /// Represents an empty frame.
29 | ///
30 | /// 31 | /// 空帧可以用来快速清空绘制完的一帧.
32 | /// An empty frame is able to clear the canvas quickly. 33 | ///
34 | protected uint* emptyFrame; 35 | protected uint* frame; 36 | /// 37 | /// 表示指向帧的索引.
38 | /// Represents indexes of the frame. 39 | ///
40 | protected uint** frameIdx; 41 | protected static readonly uint[] DefaultKeyColors = new uint[128]; 42 | public readonly uint[] KeyColors = new uint[128]; 43 | public readonly bool[] KeyPressed = new bool[128]; 44 | static CanvasBase() 45 | { 46 | for (int i = 0; i != 128; ++i) 47 | { 48 | switch (i % 12) 49 | { 50 | case 1: 51 | case 3: 52 | case 6: 53 | case 8: 54 | case 10: 55 | // 黑键 black keys 56 | DefaultKeyColors[i] = 0xFF000000; 57 | break; 58 | default: 59 | // 白键 white keys 60 | DefaultKeyColors[i] = 0xFFFFFFFF; 61 | break; 62 | } 63 | } 64 | } 65 | public CanvasBase(in RenderOptions options) 66 | { 67 | width = options.Width; 68 | height = options.Height; 69 | fps = options.FPS; 70 | crf = options.VideoQuality; 71 | keyh = options.KeyHeight; 72 | lineColor = options.DivideBarColor; 73 | frameSize = (ulong)width * (ulong)height * 4ul; 74 | background = options.BackgroundColor; 75 | 76 | Array.Copy(Global.KeyColors, Global.NoteColors, Global.KeyColors.Length); 77 | if (Global.TranslucentNotes) 78 | { 79 | double alphaRatio = Global.NoteAlpha / 255.0; 80 | for (int i = 0; i != Global.NoteColors.Length; ++i) 81 | { 82 | ref RGBAColor c = ref Global.NoteColors[i]; 83 | c.R = (byte)(background.R + Math.Round((c.R - background.R) * alphaRatio)); 84 | c.G = (byte)(background.G + Math.Round((c.G - background.G) * alphaRatio)); 85 | c.B = (byte)(background.B + Math.Round((c.B - background.B) * alphaRatio)); 86 | c.A = 0xFF; 87 | } 88 | } 89 | 90 | StringBuilder ffargs = new StringBuilder(); 91 | 92 | _ = ffargs.Append("-y -hide_banner -f rawvideo -pix_fmt rgba -s ").Append(width).Append('x') 93 | .Append(height).Append(" -r ").Append(fps).Append(" -i - "); 94 | _ = options.AdditionalFFMpegArgument.ToLower().Contains("-vcodec") ? null : ffargs.Append("-pix_fmt yuv420p -preset ultrafast"); 95 | _ = ffargs.Append(' ').Append(options.AdditionalFFMpegArgument); 96 | //_ = !options.PreviewMode ? ffargs.Append(" \"").Append(options.Output).Append("\"") : ffargs.Append(" -f sdl2 Preview"); 97 | if (options.PreviewMode) 98 | { 99 | Version osVer = Environment.OSVersion.Version; 100 | _ = osVer.Major == 6 && osVer.Minor == 1 ? ffargs.Append(" -f sdl Preview") : ffargs.Append(" -f sdl2 Preview"); 101 | } 102 | else 103 | { 104 | if (options.QualityOptions == VideoQualityOptions.CRF) 105 | { 106 | _ = ffargs.Append(" -crf ").Append(options.VideoQuality); 107 | } 108 | else 109 | { 110 | _ = ffargs.Append(" -b:v ").Append(options.VideoQuality).Append("k -maxrate ").Append(options.VideoQuality).Append("k -minrate ") 111 | .Append(options.VideoQuality).Append("k -bufsize ").Append(options.VideoQuality).Append('k'); 112 | } 113 | _ = ffargs.Append(" \"").Append(options.Output).Append("\""); 114 | } 115 | pipe = new FFMpeg(ffargs.ToString(), width, height); 116 | 117 | frame = (uint*)UnsafeMemory.Allocate(frameSize + ((uint)width * 4ul)); // malloc 118 | // 使用此方法: Call UnsafeMemory.Set in order to: 119 | // 清空分配的帧, 将它们全部初始化为0. Clear the newly allocated frame, filling it with 0. 120 | UnsafeMemory.Set(frame, 0, frameSize); // memset 121 | 122 | frameIdx = (uint**)UnsafeMemory.Allocate((ulong)height * (ulong)sizeof(void*)); 123 | for (int i = 0; i != height; ++i) 124 | { 125 | frameIdx[i] = frame + ((height - i - 1) * width); 126 | } 127 | emptyFrame = (uint*)UnsafeMemory.Allocate(frameSize); 128 | uint backgroundColor = options.TransparentBackground ? (options.BackgroundColor & 0x00FFFFFF) : (uint)options.BackgroundColor; 129 | for (uint i = 0, loop = (uint)frameSize / 4; i != loop; ++i) 130 | { 131 | frame[i] = backgroundColor; 132 | } 133 | UnsafeMemory.Copy(emptyFrame, frame, frameSize); 134 | fixed (bool* kp = KeyPressed) 135 | { 136 | UnsafeMemory.Set(kp, 0, 128); 137 | } 138 | } 139 | 140 | public void Dispose() 141 | { 142 | pipe.Dispose(); 143 | if (frame != null) 144 | { 145 | UnsafeMemory.Free(frame); 146 | } 147 | if (emptyFrame != null) 148 | { 149 | UnsafeMemory.Free(emptyFrame); 150 | } 151 | if (frameIdx != null) 152 | { 153 | UnsafeMemory.Free(frameIdx); 154 | } 155 | frame = null; 156 | emptyFrame = null; 157 | frameIdx = null; 158 | } 159 | 160 | /// 161 | /// 绘制矩形边框.
162 | /// Draws a rectangle which is not filled with specified color. 163 | ///
164 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 165 | public void DrawRectangle(int x, int y, int width, int height, uint color) 166 | { 167 | 168 | int i; 169 | //if (x < _Width) 170 | for (i = y; i < y + height; ++i) 171 | { 172 | frameIdx[i][x] = color; 173 | } 174 | //if (y < _Height) 175 | for (i = x; i < x + width; ++i) 176 | { 177 | frameIdx[y][i] = color; 178 | } 179 | //if (w > 1) 180 | for (i = y; i < y + height; ++i) 181 | { 182 | frameIdx[i][x + width - 1] = color; 183 | } 184 | //if (h > 1) 185 | for (i = x; i < x + width; ++i) 186 | { 187 | frameIdx[y + height - 1][i] = color; 188 | } 189 | } 190 | 191 | /// 192 | /// 用指定的颜色填满指定区域.
193 | /// Fill specified area of the frame with given color. 194 | ///
195 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 196 | public void FillRectangle(int x, int y, int width, int height, uint color) 197 | { 198 | for (int i = x, xend = x + width; i != xend; ++i) 199 | { 200 | for (int j = y, yend = y + height; j != yend; ++j) 201 | { 202 | frameIdx[j][i] = color; 203 | } 204 | } 205 | } 206 | /// 207 | /// 清空当前画布.
208 | /// Clear the canvas immediately. 209 | ///
210 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 211 | public void Clear() 212 | { 213 | fixed (uint* dst = KeyColors, src = DefaultKeyColors) 214 | { 215 | UnsafeMemory.Copy(dst, src, 512); 216 | } 217 | UnsafeMemory.Copy(frame, emptyFrame, frameSize); 218 | fixed (bool* kp = KeyPressed) 219 | { 220 | UnsafeMemory.Set(kp, 0, 128); 221 | } 222 | } 223 | /// 224 | /// 向 FFMpeg 写入当前帧.
225 | /// Write current frame to ffmpeg. 226 | ///
227 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 228 | public void WriteFrame() 229 | { 230 | pipe.WriteFrame(frame); 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /Core/RenderFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Runtime.CompilerServices; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using SharpExtension.IO; 10 | using SharpExtension; 11 | using SharpExtension.Collections; 12 | 13 | namespace QQS_UI.Core 14 | { 15 | public unsafe struct MidiStream : IDisposable 16 | { 17 | private readonly CStream stream; 18 | 19 | public MidiStream(string path) 20 | { 21 | stream = CStream.OpenFile(path, "rb"); 22 | } 23 | 24 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 25 | public ushort ReadInt16() 26 | { 27 | int hi = stream.GetChar(); 28 | return (ushort)((hi << 8) | stream.GetChar()); 29 | } 30 | 31 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 32 | public uint ReadInt32() 33 | { 34 | int h1, h2, h3; 35 | h1 = stream.GetChar(); 36 | h2 = stream.GetChar(); 37 | h3 = stream.GetChar(); 38 | return (uint)((h1 << 24) | (h2 << 16) | (h3 << 8) | stream.GetChar()); 39 | } 40 | 41 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 42 | public byte ReadInt8() 43 | { 44 | return (byte)stream.GetChar(); 45 | } 46 | 47 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 48 | public ulong Read(void* contentBuffer, ulong size, ulong count) 49 | { 50 | return stream.ReadWithoutLock(contentBuffer, size, count); 51 | } 52 | 53 | public bool Associated 54 | { 55 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 56 | get => !stream.Closed; 57 | } 58 | 59 | public void Dispose() 60 | { 61 | stream.Dispose(); 62 | GC.SuppressFinalize(this); 63 | } 64 | } 65 | internal unsafe struct RenderTrackInfo 66 | { 67 | public byte* Data; 68 | public ulong Size; 69 | public uint TrackTime; 70 | public UnmanagedList Notes; 71 | } 72 | public unsafe class RenderFile 73 | { 74 | public ushort TrackCount; 75 | public ushort Division; 76 | public uint MidiTime = 0; 77 | public long NoteCount = 0; 78 | public UnmanagedList[] Notes = new UnmanagedList[128]; 79 | public UnmanagedList Tempos = new UnmanagedList(); 80 | private void Parse() 81 | { 82 | MidiStream stream = new MidiStream(MidiPath); 83 | sbyte* hdr = stackalloc sbyte[4]; 84 | 85 | _ = stream.Read(hdr, 4, 1); 86 | // 判断是否与MThd相等 87 | if (!(hdr[0] == 'M' && hdr[1] == 'T' && hdr[2] == 'h' && hdr[3] == 'd')) 88 | { 89 | throw new Exception(); 90 | } 91 | uint hdrSize = stream.ReadInt32(); 92 | if (hdrSize != 6) 93 | { 94 | throw new Exception(); 95 | } 96 | if (stream.ReadInt16() == 2) 97 | { 98 | throw new Exception(); 99 | } 100 | TrackCount = stream.ReadInt16(); 101 | Division = stream.ReadInt16(); 102 | if (Division > 32767) 103 | { 104 | throw new Exception(); 105 | } 106 | 107 | RenderTrackInfo[] trkInfo = new RenderTrackInfo[TrackCount]; 108 | for (int i = 0; i != TrackCount; ++i) 109 | { 110 | _ = stream.Read(hdr, 4, 1); 111 | if (!(hdr[0] == 'M' && hdr[1] == 'T' && hdr[2] == 'r' && hdr[3] == 'k')) 112 | { 113 | throw new Exception(); 114 | } 115 | trkInfo[i] = new RenderTrackInfo 116 | { 117 | Size = stream.ReadInt32(), 118 | Notes = new UnmanagedList(), 119 | TrackTime = 0, 120 | }; 121 | trkInfo[i].Data = (byte*)UnsafeMemory.Allocate(trkInfo[i].Size); 122 | _ = stream.Read(trkInfo[i].Data, trkInfo[i].Size, 1); 123 | Console.WriteLine("拷贝音轨 #{0} 的信息. 音轨大小: {1} 字节.", i, trkInfo[i].Size); 124 | } 125 | stream.Dispose(); 126 | for (int i = 0; i != 128; ++i) 127 | { 128 | Notes[i] = new UnmanagedList(); 129 | } 130 | ParallelOptions opt = new() 131 | { 132 | MaxDegreeOfParallelism = Global.MaxMIDILoaderConcurrency 133 | }; 134 | _ = Parallel.For(0, TrackCount, opt, (i) => 135 | { 136 | UnmanagedList nl = trkInfo[i].Notes; // pass by ref 137 | ForwardLinkedList[] fll = new ForwardLinkedList[128]; 138 | for (int j = 0; j != 128; ++j) 139 | { 140 | fll[j] = new ForwardLinkedList(); 141 | } 142 | 143 | uint trkTime = 0; 144 | bool loop = true; 145 | byte* p = trkInfo[i].Data; 146 | byte prev = 0; 147 | byte comm; 148 | while (loop) 149 | { 150 | trkTime += Global.ParseVLInt(ref p); 151 | comm = *p++; 152 | if (comm < 0x80) 153 | { 154 | comm = prev; 155 | --p; 156 | } 157 | prev = comm; 158 | switch (comm & 0b11110000) 159 | { 160 | case 0x80: 161 | { 162 | byte k = *p++; 163 | ++p; 164 | if (fll[k].Any()) 165 | { 166 | long idx = fll[k].Pop(); 167 | nl[idx].End = trkTime; 168 | } 169 | } 170 | continue; 171 | case 0x90: 172 | { 173 | byte k = *p++; 174 | byte v = *p++; 175 | if (v != 0) 176 | { 177 | long idx = nl.Count; 178 | fll[k].Add(idx); 179 | nl.Add(new Note 180 | { 181 | Start = trkTime, 182 | Key = k, 183 | Track = (ushort)i 184 | }); 185 | } 186 | else 187 | { 188 | if (fll[k].Any()) 189 | { 190 | long idx = fll[k].Pop(); 191 | nl[idx].End = trkTime; 192 | } 193 | } 194 | } 195 | continue; 196 | case 0xA0: 197 | case 0xB0: 198 | case 0xE0: 199 | p += 2; 200 | continue; 201 | case 0xC0: 202 | case 0xD0: 203 | ++p; 204 | continue; 205 | default: 206 | break; 207 | } 208 | switch (comm) 209 | { 210 | case 0xF0: 211 | while (*p++ != 0xF7) 212 | { 213 | 214 | } 215 | continue; 216 | case 0xF1: 217 | continue; 218 | case 0xF2: 219 | case 0xF3: 220 | p += 0xF4 - comm; 221 | continue; 222 | default: 223 | break; 224 | } 225 | if (comm < 0xFF) 226 | { 227 | continue; 228 | } 229 | comm = *p++; 230 | if (comm >= 0 && comm <= 0x0A) 231 | { 232 | uint l = Global.ParseVLInt(ref p); // 这个中间变量不可以去掉 233 | p += l; 234 | continue; 235 | } 236 | switch (comm) 237 | { 238 | case 0x2F: 239 | ++p; 240 | for (int k = 0; k != 128; ++k) 241 | { 242 | foreach (long l in fll[k]) 243 | { 244 | nl[l].End = trkTime; 245 | } 246 | } 247 | loop = false; 248 | break; 249 | case 0x51: 250 | _ = Global.ParseVLInt(ref p); 251 | byte b1 = *p++; 252 | byte b2 = *p++; 253 | uint t = (uint)((b1 << 16) | (b2 << 8) | (*p++)); 254 | lock (Tempos) 255 | { 256 | Tempos.Add(new Tempo 257 | { 258 | Tick = trkTime, 259 | Value = t 260 | }); 261 | } 262 | break; 263 | default: 264 | uint dl = Global.ParseVLInt(ref p); // 这个中间变量不可以去掉 265 | p += dl; 266 | break; 267 | } 268 | } 269 | Console.WriteLine("音轨 #{0} 解析完成. 音符数: {1}.", i, nl.Count); 270 | UnsafeMemory.Free(trkInfo[i].Data); 271 | trkInfo[i].Data = null; 272 | trkInfo[i].TrackTime = trkTime; 273 | }); 274 | for (int i = 0; i != TrackCount; ++i) 275 | { 276 | if (trkInfo[i].TrackTime > MidiTime) 277 | { 278 | MidiTime = trkInfo[i].TrackTime; 279 | } 280 | NoteCount += trkInfo[i].Notes.Count; 281 | } 282 | Tempos.TrimExcess(); 283 | Console.WriteLine("正在处理 Midi 事件..."); 284 | for (int i = 0; i != TrackCount; ++i) 285 | { 286 | if (trkInfo[i].Notes.Count == 0) 287 | { 288 | continue; 289 | } 290 | for (long j = 0, len = trkInfo[i].Notes.Count; j != len; ++j) 291 | { 292 | ref Note n = ref trkInfo[i].Notes[j]; 293 | Notes[n.Key].Add(n); 294 | } 295 | trkInfo[i].Notes.Clear(); 296 | } 297 | _ = Parallel.ForEach(Notes, (nl) => 298 | { 299 | nl.TrimExcess(); 300 | NoteSorter.Sort(nl); 301 | }); 302 | if (Tempos.Count == 0) 303 | { 304 | Tempos.Add(new Tempo 305 | { 306 | Tick = 0, 307 | Value = 500000 308 | }); 309 | } 310 | // sort tempos 311 | Tempo[] temp = Tempos.ToManaged(); 312 | Array.Sort(temp, (left, right) => 313 | { 314 | return left.Tick < right.Tick ? -1 : left.Tick == right.Tick ? 0 : 1; 315 | }); 316 | UnmanagedArray arr = Interoperability.MakeUnmanagedArray(temp); 317 | Tempos.Clear(); 318 | Tempos.AddRange(arr); 319 | arr.Dispose(); 320 | 321 | Console.WriteLine("Midi 事件处理完成. 音符总数: {0}.", NoteCount); 322 | Console.WriteLine("正在对 Midi 文件进行 OR 处理."); 323 | _ = Parallel.For(0, 128, opt, [MethodImpl(MethodImplOptions.AggressiveOptimization)] (i) => 324 | { 325 | UnmanagedList nl = Notes[i]; 326 | if (nl.Count < 10) 327 | { 328 | return; 329 | } 330 | Note* pnl = (Note*)UnsafeMemory.GetActualAddressOf(ref nl[0]); 331 | for (long index = 0, len = nl.Count - 2; index != len;) 332 | { 333 | ref Note curr = ref pnl[index++]; 334 | ref Note next = ref pnl[index]; 335 | if (curr.Start < next.Start && curr.End > next.Start && curr.End < next.End) 336 | { 337 | curr.End = next.Start; 338 | } 339 | else if (curr.Start == next.Start && curr.End <= next.End) 340 | { 341 | curr.End = curr.Start; 342 | } 343 | } 344 | }); 345 | Console.WriteLine("OR 处理完成."); 346 | } 347 | public RenderFile(string path) 348 | { 349 | MidiPath = path; 350 | if (!File.Exists(path)) 351 | { 352 | throw new FileNotFoundException(); 353 | } 354 | Stopwatch sw = Stopwatch.StartNew(); 355 | Parse(); 356 | sw.Stop(); 357 | Console.WriteLine("加载 Midi 用时: {0:F2} s.", sw.ElapsedMilliseconds / 1000.0); 358 | } 359 | 360 | public string MidiPath { get; } 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /Core/CommonRenderer.cs: -------------------------------------------------------------------------------- 1 | using SharpExtension; 2 | using SharpExtension.Collections; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.Linq; 7 | using System.Runtime.CompilerServices; 8 | using System.Text; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace QQS_UI.Core 13 | { 14 | public sealed class CommonRenderer : RendererBase 15 | { 16 | private readonly CommonCanvas canvas; 17 | private readonly bool drawMiddleSquare; 18 | private readonly bool gradientNotes; 19 | private readonly bool thinnerNotes; 20 | private readonly bool whiteKeyShade; 21 | private readonly double delayStart; 22 | private readonly ParallelOptions parallelOptions; 23 | public CommonRenderer(RenderFile file, in RenderOptions options) : base(file, options) 24 | { 25 | canvas = new CommonCanvas(options); 26 | drawMiddleSquare = options.DrawGreySquare; 27 | gradientNotes = options.Gradient; 28 | thinnerNotes = options.ThinnerNotes; 29 | delayStart = options.DelayStartSeconds; 30 | whiteKeyShade = options.WhiteKeyShade; 31 | 32 | parallelOptions = new ParallelOptions 33 | { 34 | MaxDegreeOfParallelism = Global.MaxRenderConcurrency 35 | }; 36 | } 37 | 38 | public override void Render() 39 | { 40 | if (thinnerNotes) 41 | { 42 | Render_Thinner(); 43 | } 44 | else 45 | { 46 | Render_Wider(); 47 | } 48 | } 49 | 50 | [MethodImpl(MethodImplOptions.AggressiveOptimization)] 51 | private unsafe void Render_Thinner() 52 | { 53 | // Pixels per beat. 54 | // eval "spd": 55 | // spd = 1000000.0 * ppq / [tempo] / fps 56 | double ppb = 520.0 / ppq * noteSpeed; 57 | double fileTick = renderFile.MidiTime; 58 | double tick = 0, tickup, spd = ppq * 2.0 / fps; 59 | 60 | long spdidx = 0; 61 | long spdCount = tempos.Count; 62 | double deltaTicks = (height - keyHeight) / ppb; 63 | 64 | Note** noteBegins = stackalloc Note*[128]; 65 | Note** end = stackalloc Note*[128]; 66 | 67 | Stopwatch frameWatch = new Stopwatch(); 68 | double frameLen = 10000000.0 / fps; 69 | 70 | int middleCx = canvas.GetKeyX(60); 71 | int middleCwidth = canvas.GetKeyWidth(60); 72 | int greySquareY = (int)keyHeight * 2 / 15; 73 | int greySquareLeft = middleCx + (middleCwidth / 4); 74 | int greySquareWidth = middleCwidth * 2 / 4; 75 | for (int i = 0; i != 128; ++i) 76 | { 77 | if (noteMap[i].Count != 0) 78 | { 79 | noteBegins[i] = (Note*)UnsafeMemory.GetActualAddressOf(ref noteMap[i][0]); 80 | end[i] = noteBegins[i] + noteMap[i].Count; 81 | } 82 | else 83 | { 84 | noteBegins[i] = end[i] = null; 85 | } 86 | } 87 | int colorLen = Global.KeyColors.Length; 88 | 89 | int delayFrames = (int)(delayStart * fps); 90 | canvas.Clear(); 91 | if (gradientNotes) 92 | { 93 | canvas.DrawGradientKeys(); 94 | } 95 | else 96 | { 97 | canvas.DrawKeys(); 98 | } 99 | for (int i = 0; i != delayFrames; ++i) 100 | { 101 | canvas.WriteFrame(); 102 | } 103 | for (; tick < fileTick; tick += spd) 104 | { 105 | frameWatch.Restart(); 106 | while (isPreview && Global.PreviewPaused && !Interrupt) 107 | { 108 | canvas.WriteFrame(); 109 | while (Global.LimitPreviewFPS && frameWatch.ElapsedTicks < frameLen) 110 | { 111 | 112 | } 113 | frameWatch.Restart(); 114 | } 115 | canvas.Clear(); 116 | tickup = tick + deltaTicks; 117 | while (spdidx != spdCount && tempos[spdidx].Tick < tick) 118 | { 119 | spd = 1e6 / tempos[spdidx].Value * ppq / fps; 120 | ++spdidx; 121 | } 122 | if (Interrupt) 123 | { 124 | break; 125 | } 126 | // 使用并行 for 循环提高性能. 127 | _ = Parallel.For(0, 128, parallelOptions, 128 | [MethodImpl(MethodImplOptions.AggressiveOptimization)] 129 | (i) => 130 | { 131 | if (noteBegins[i] == null) 132 | { 133 | return; // no notes available 134 | } 135 | uint j, k, l; 136 | bool flag = false; 137 | Note* noteptr = noteBegins[i]; 138 | bool isCurrentNotePressed; 139 | while (noteptr->Start < tickup) 140 | { 141 | isCurrentNotePressed = false; 142 | if (noteptr == end[i]) 143 | { 144 | break; 145 | } 146 | if (noteptr->End >= tick) 147 | { 148 | l = Global.KeyColors[noteptr->Track % colorLen]; 149 | if (!flag && (flag = true)) 150 | { 151 | noteBegins[i] = noteptr; 152 | } 153 | if (noteptr->Start < tick) 154 | { 155 | k = keyHeight; 156 | j = (uint)((noteptr->End - tick) * ppb); 157 | canvas.KeyColors[i] = l; 158 | canvas.KeyPressed[i] = true; 159 | canvas.KeyTracks[i] = noteptr->Track; 160 | isCurrentNotePressed = true; 161 | } 162 | else 163 | { 164 | k = (uint)(((noteptr->Start - tick) * ppb) + keyHeight); 165 | j = (uint)((noteptr->End - noteptr->Start) * ppb); 166 | } 167 | if (j + k > height) 168 | { 169 | j = height - k; 170 | } 171 | l = Global.NoteColors[noteptr->Track % colorLen]; 172 | if (gradientNotes) 173 | { 174 | canvas.DrawGradientNote((short)i, noteptr->Track % colorLen, (int)k, (int)j, isCurrentNotePressed); 175 | } 176 | else 177 | { 178 | canvas.DrawNote((short)i, noteptr->Track % colorLen, (int)k, (int)j, l, isCurrentNotePressed); // each key is individual 179 | } 180 | } 181 | ++noteptr; 182 | } 183 | }); 184 | if (gradientNotes) 185 | { 186 | canvas.DrawGradientKeys(); 187 | } 188 | else 189 | { 190 | canvas.DrawKeys(); 191 | } 192 | if (drawMiddleSquare) 193 | { 194 | RGBAColor col = canvas.KeyColors[60]; 195 | col.R = (byte)Math.Round(col.R * 0.62745); 196 | col.G = (byte)Math.Round(col.G * 0.62745); 197 | col.B = (byte)Math.Round(col.B * 0.62745); 198 | if (whiteKeyShade && canvas.KeyPressed[60]) 199 | { 200 | canvas.FillRectangle(greySquareLeft, greySquareY - ((int)keyHeight / 50), greySquareWidth, greySquareWidth, col); 201 | } 202 | else 203 | { 204 | canvas.FillRectangle(greySquareLeft, greySquareY, greySquareWidth, greySquareWidth, col); 205 | } 206 | } 207 | canvas.WriteFrame(); 208 | if (isPreview && Global.LimitPreviewFPS) 209 | { 210 | while (frameWatch.ElapsedTicks < frameLen) 211 | { 212 | 213 | } 214 | } 215 | } 216 | canvas.Clear(); 217 | if (gradientNotes) 218 | { 219 | canvas.DrawGradientKeys(); 220 | } 221 | else 222 | { 223 | canvas.DrawKeys(); 224 | } 225 | for (int i = 0; i != 3 * fps; i++) 226 | { 227 | if (Interrupt) 228 | { 229 | break; 230 | } 231 | canvas.WriteFrame(); 232 | } 233 | canvas.Dispose(); 234 | } 235 | 236 | [MethodImpl(MethodImplOptions.AggressiveOptimization)] 237 | private unsafe void Render_Wider() 238 | { 239 | // Pixels per beat. 240 | // eval "spd": 241 | // spd = 1000000.0 * ppq / [tempo] / fps 242 | double ppb = 520.0 / ppq * noteSpeed; 243 | double fileTick = renderFile.MidiTime; 244 | double tick = 0, tickup, spd = ppq * 2.0 / fps; 245 | 246 | long spdidx = 0; 247 | long spdCount = tempos.Count; 248 | double deltaTicks = (height - keyHeight) / ppb; 249 | 250 | Note** noteBegins = stackalloc Note*[128]; 251 | Note** end = stackalloc Note*[128]; 252 | 253 | Stopwatch frameWatch = new Stopwatch(); 254 | double frameLen = 10000000.0 / fps; 255 | 256 | int middleCx = canvas.GetKeyX(60); 257 | int middleCwidth = canvas.GetKeyWidth(60); 258 | int greySquareY = (int)keyHeight * 2 / 15; 259 | int greySquareLeft = middleCx + (middleCwidth / 4); 260 | int greySquareWidth = middleCwidth * 2 / 4; 261 | for (int i = 0; i != 128; ++i) 262 | { 263 | if (noteMap[i].Count != 0) 264 | { 265 | noteBegins[i] = (Note*)UnsafeMemory.GetActualAddressOf(ref noteMap[i][0]); 266 | end[i] = noteBegins[i] + noteMap[i].Count; 267 | } 268 | else 269 | { 270 | noteBegins[i] = end[i] = null; 271 | } 272 | } 273 | int colorLen = Global.KeyColors.Length; 274 | int delayFrames = (int)delayStart * fps; 275 | canvas.Clear(); 276 | if (gradientNotes) 277 | { 278 | canvas.DrawGradientKeys(); 279 | } 280 | else 281 | { 282 | canvas.DrawKeys(); 283 | } 284 | for (int i = 0; i != delayFrames; ++i) 285 | { 286 | canvas.WriteFrame(); 287 | } 288 | for (; tick < fileTick; tick += spd) 289 | { 290 | frameWatch.Restart(); 291 | while (isPreview && Global.PreviewPaused && !Interrupt) 292 | { 293 | canvas.WriteFrame(); 294 | while (Global.LimitPreviewFPS && frameWatch.ElapsedTicks < frameLen) 295 | { 296 | 297 | } 298 | frameWatch.Restart(); 299 | } 300 | canvas.Clear(); 301 | tickup = tick + deltaTicks; 302 | while (spdidx != spdCount && tempos[spdidx].Tick < tick) 303 | { 304 | spd = 1e6 / tempos[spdidx].Value * ppq / fps; 305 | ++spdidx; 306 | } 307 | if (Interrupt) 308 | { 309 | break; 310 | } 311 | // 使用并行 for 循环提高性能. 312 | _ = Parallel.For(0, 75, parallelOptions, [MethodImpl(MethodImplOptions.AggressiveOptimization)] (i) => 313 | { 314 | i = Global.DrawMap[i]; 315 | if (noteBegins[i] == null) 316 | { 317 | return; // no notes available 318 | } 319 | uint j, k, l; 320 | bool flag = false; 321 | bool isCurrentNotePressed; 322 | Note* noteptr = noteBegins[i]; 323 | while (noteptr->Start < tickup) 324 | { 325 | isCurrentNotePressed = false; 326 | if (noteptr == end[i]) 327 | { 328 | break; 329 | } 330 | if (noteptr->End >= tick) 331 | { 332 | l = Global.KeyColors[noteptr->Track % colorLen]; 333 | if (!flag && (flag = true)) 334 | { 335 | noteBegins[i] = noteptr; 336 | } 337 | if (noteptr->Start < tick) 338 | { 339 | k = keyHeight; 340 | j = (uint)((noteptr->End - tick) * ppb); 341 | canvas.KeyColors[i] = l; 342 | canvas.KeyPressed[i] = true; 343 | canvas.KeyTracks[i] = noteptr->Track; 344 | isCurrentNotePressed = true; 345 | } 346 | else 347 | { 348 | k = (uint)(((noteptr->Start - tick) * ppb) + keyHeight); 349 | j = (uint)((noteptr->End - noteptr->Start) * ppb); 350 | } 351 | if (j + k > height) 352 | { 353 | j = height - k; 354 | } 355 | if (gradientNotes) 356 | { 357 | canvas.DrawGradientNote((short)i, noteptr->Track % colorLen, (int)k, (int)j, isCurrentNotePressed); 358 | } 359 | else 360 | { 361 | canvas.DrawNote((short)i, noteptr->Track % colorLen, (int)k, (int)j, l, isCurrentNotePressed); // each key is individual 362 | } 363 | } 364 | ++noteptr; 365 | } 366 | }); 367 | _ = Parallel.For(75, 128, parallelOptions, [MethodImpl(MethodImplOptions.AggressiveOptimization)] (i) => 368 | { 369 | i = Global.DrawMap[i]; 370 | if (noteBegins[i] == null) 371 | { 372 | return; // no notes available 373 | } 374 | uint j, k, l; 375 | bool flag = false; 376 | bool isCurrentNotePressed; 377 | Note* noteptr = noteBegins[i]; 378 | while (noteptr->Start < tickup) 379 | { 380 | isCurrentNotePressed = false; 381 | if (noteptr == end[i]) 382 | { 383 | break; 384 | } 385 | if (noteptr->End >= tick) 386 | { 387 | l = Global.KeyColors[noteptr->Track % colorLen]; 388 | if (!flag && (flag = true)) 389 | { 390 | noteBegins[i] = noteptr; 391 | } 392 | if (noteptr->Start < tick) 393 | { 394 | k = keyHeight; 395 | j = (uint)((noteptr->End - tick) * ppb); 396 | canvas.KeyColors[i] = l; 397 | canvas.KeyPressed[i] = true; 398 | canvas.KeyTracks[i] = noteptr->Track; 399 | isCurrentNotePressed = true; 400 | } 401 | else 402 | { 403 | k = (uint)(((noteptr->Start - tick) * ppb) + keyHeight); 404 | j = (uint)((noteptr->End - noteptr->Start) * ppb); 405 | } 406 | if (j + k > height) 407 | { 408 | j = height - k; 409 | } 410 | if (gradientNotes) 411 | { 412 | canvas.DrawGradientNote((short)i, noteptr->Track % colorLen, (int)k, (int)j, isCurrentNotePressed); 413 | } 414 | else 415 | { 416 | canvas.DrawNote((short)i, noteptr->Track % colorLen, (int)k, (int)j, l, isCurrentNotePressed); // each key is individual 417 | } 418 | } 419 | ++noteptr; 420 | } 421 | }); 422 | if (gradientNotes) 423 | { 424 | canvas.DrawGradientKeys(); 425 | } 426 | else 427 | { 428 | canvas.DrawKeys(); 429 | } 430 | if (drawMiddleSquare) 431 | { 432 | RGBAColor col = canvas.KeyColors[60]; 433 | col.R = (byte)Math.Round(col.R * 0.62745); 434 | col.G = (byte)Math.Round(col.G * 0.62745); 435 | col.B = (byte)Math.Round(col.B * 0.62745); 436 | if (whiteKeyShade && canvas.KeyPressed[60]) 437 | { 438 | canvas.FillRectangle(greySquareLeft, greySquareY - ((int)keyHeight / 50), greySquareWidth, greySquareWidth, col); 439 | } 440 | else 441 | { 442 | canvas.FillRectangle(greySquareLeft, greySquareY, greySquareWidth, greySquareWidth, col); 443 | } 444 | } 445 | canvas.WriteFrame(); 446 | if (isPreview && Global.LimitPreviewFPS) 447 | { 448 | while (frameWatch.ElapsedTicks < frameLen) 449 | { 450 | 451 | } 452 | } 453 | } 454 | canvas.Clear(); 455 | if (gradientNotes) 456 | { 457 | canvas.DrawGradientKeys(); 458 | } 459 | else 460 | { 461 | canvas.DrawKeys(); 462 | } 463 | for (int i = 0; i != 3 * fps; i++) 464 | { 465 | if (Interrupt) 466 | { 467 | break; 468 | } 469 | canvas.WriteFrame(); 470 | } 471 | canvas.Dispose(); 472 | } 473 | 474 | public void Dispose() 475 | { 476 | canvas.Dispose(); 477 | GC.SuppressFinalize(this); 478 | } 479 | 480 | ~CommonRenderer() 481 | { 482 | Dispose(); 483 | } 484 | } 485 | } 486 | -------------------------------------------------------------------------------- /UI/Material.xaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 22 | 55 | 84 | 112 | 140 | 164 | 232 | 233 | 288 | 343 | 364 | 365 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | -------------------------------------------------------------------------------- /Core/CommonCanvas.cs: -------------------------------------------------------------------------------- 1 | using SharpExtension; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Runtime.CompilerServices; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace QQS_UI.Core 10 | { 11 | public sealed unsafe class CommonCanvas : CanvasBase 12 | { 13 | private readonly RGBAColor[][][] NoteGradients = new RGBAColor[128][][]; // 补充: 感谢 Tweak 为渐变音符做出的贡献 14 | private readonly RGBAColor[][][] BrightenedNoteGradients = new RGBAColor[128][][]; // 用于存储处理过的渐变色 15 | private readonly RGBAColor[][] PressedWhiteKeyGradients; 16 | private readonly RGBAColor[] UnpressedWhiteKeyGradients; 17 | private readonly RGBAColor[] BorderColors; 18 | private readonly RGBAColor[] DenseNoteColors; 19 | public readonly ushort[] KeyTracks = new ushort[128]; 20 | private readonly bool enableGradient; 21 | private readonly bool separator; 22 | private readonly bool betterBlackKeys; 23 | private readonly bool whiteKeyShade; 24 | private readonly bool brighterNotesOnHit; 25 | private readonly byte pressedNotesShadeDecrement; 26 | private readonly int borderWidth, borderHeight; 27 | private readonly HorizontalGradientDirection noteGradientDirection; 28 | private readonly VerticalGradientDirection separatorGradientDirection, keyboardGradientDirection; 29 | public CommonCanvas(in RenderOptions options) : base(options) 30 | { 31 | enableGradient = options.Gradient; 32 | separator = options.DrawSeparator; 33 | noteGradientDirection = options.NoteGradientDirection; 34 | separatorGradientDirection = options.SeparatorGradientDirection; 35 | keyboardGradientDirection = options.KeyboardGradientDirection; 36 | betterBlackKeys = options.BetterBlackKeys; 37 | whiteKeyShade = options.WhiteKeyShade; 38 | brighterNotesOnHit = options.BrighterNotesOnHit; 39 | pressedNotesShadeDecrement = (byte)options.PressedNotesShadeDecrement; 40 | 41 | if (pressedNotesShadeDecrement == 0) 42 | { 43 | brighterNotesOnHit = false; 44 | } 45 | 46 | borderWidth = Global.EnableNoteBorder ? Math.Max((int)Math.Round(0.0006 * Global.NoteBorderWidth * width), 1) : 0; 47 | borderHeight = (int)Math.Round((double)borderWidth / width * height); 48 | 49 | if (Global.EnableNoteBorder) 50 | { 51 | BorderColors = new RGBAColor[Global.KeyColors.Length]; 52 | Array.Copy(Global.KeyColors, BorderColors, Global.KeyColors.Length); 53 | for (int i = 0; i != BorderColors.Length; ++i) 54 | { 55 | ref RGBAColor col = ref BorderColors[i]; 56 | col.R = (byte)Math.Round(col.R / Global.NoteBorderShade); 57 | col.G = (byte)Math.Round(col.G / Global.NoteBorderShade); 58 | col.B = (byte)Math.Round(col.B / Global.NoteBorderShade); 59 | } 60 | } 61 | if (Global.EnableDenseNoteEffect) 62 | { 63 | DenseNoteColors = new RGBAColor[Global.KeyColors.Length]; 64 | Array.Copy(Global.KeyColors, DenseNoteColors, Global.KeyColors.Length); 65 | for (int i = 0; i != DenseNoteColors.Length; ++i) 66 | { 67 | ref RGBAColor col = ref DenseNoteColors[i]; 68 | col.R = (byte)Math.Round(col.R / Global.DenseNoteShade); 69 | col.G = (byte)Math.Round(col.G / Global.DenseNoteShade); 70 | col.B = (byte)Math.Round(col.B / Global.DenseNoteShade); 71 | } 72 | } 73 | 74 | UnpressedWhiteKeyGradients = new RGBAColor[keyh - (whiteKeyShade ? (keyh / 20) : 0)]; 75 | for (int i = 0; i != 128; ++i) 76 | { 77 | keyx[i] = ((i / 12 * 126) + Global.GenKeyX[i % 12]) * width / 1350; 78 | } 79 | for (int i = 0; i != 127; ++i) 80 | { 81 | int val; 82 | switch (i % 12) 83 | { 84 | case 1: 85 | case 3: 86 | case 6: 87 | case 8: 88 | case 10: 89 | val = width * 9 / 1350; 90 | break; 91 | case 4: 92 | case 11: 93 | val = keyx[i + 1] - keyx[i]; 94 | break; 95 | default: 96 | val = keyx[i + 2] - keyx[i]; 97 | break; 98 | } 99 | keyw[i] = val; 100 | } 101 | keyw[127] = width - keyx[127]; 102 | 103 | if (options.ThinnerNotes) 104 | { 105 | for (int i = 0; i != 127; ++i) 106 | { 107 | switch (i % 12) 108 | { 109 | case 1: 110 | case 3: 111 | case 6: 112 | case 8: 113 | case 10: 114 | notew[i] = keyw[i]; 115 | break; 116 | case 0: 117 | case 5: 118 | notew[i] = keyx[i + 1] - keyx[i]; 119 | break; 120 | default: 121 | notew[i] = keyx[i + 1] - keyx[i - 1] - keyw[i - 1]; 122 | break; 123 | } 124 | } 125 | for (int i = 0; i != 127; ++i) 126 | { 127 | switch (i % 12) 128 | { 129 | case 0: 130 | case 5: 131 | case 1: 132 | case 3: 133 | case 6: 134 | case 8: 135 | case 10: 136 | notex[i] = keyx[i]; 137 | break; 138 | default: 139 | notex[i] = keyx[i - 1] + notew[i - 1]; 140 | break; 141 | } 142 | } 143 | notex[127] = keyx[126] + keyw[126]; 144 | notew[127] = width - notex[127]; 145 | } 146 | else 147 | { 148 | Array.Copy(keyx, notex, 128); 149 | Array.Copy(keyw, notew, 128); 150 | } 151 | 152 | // 音符不透明度与255.0的比值 153 | double alphaRatio = Global.NoteAlpha / 255.0; 154 | // 末颜色与初颜色的比值 155 | double referenceGradientRatio = Math.Pow(Global.NoteGradientScale, 10); 156 | for (int i = 0; i != 128; ++i) 157 | { 158 | // 初始化索引为i的琴键对应的颜色数组. 159 | NoteGradients[i] = new RGBAColor[Global.KeyColors.Length][]; 160 | if (brighterNotesOnHit) 161 | { 162 | BrightenedNoteGradients[i] = new RGBAColor[Global.KeyColors.Length][]; 163 | } 164 | for (int j = 0; j != Global.KeyColors.Length; ++j) 165 | { 166 | // 创建大小为音符宽度的数组, 这样就能实现渐变 167 | NoteGradients[i][j] = new RGBAColor[notew[i] - (2 * borderWidth)]; 168 | if (brighterNotesOnHit) 169 | { 170 | // 同时地, 我们创建一个相同长度的数组, 用于实现当对应音符按下时变化后的颜色. 171 | BrightenedNoteGradients[i][j] = new RGBAColor[NoteGradients[i][j].Length]; 172 | } 173 | // cols是一个引用, 没有发生值的复制 174 | RGBAColor[] cols = NoteGradients[i][j]; 175 | // 初颜色 176 | RGBAColor gradientStart = Global.KeyColors[j]; 177 | double r = gradientStart.R, g = gradientStart.G, b = gradientStart.B; 178 | // 根据实际音符宽度计算每2个像素之间颜色之比. 179 | double actualGradientRatio = Math.Pow(referenceGradientRatio, 1.0 / (notew[i] - 1)); 180 | 181 | for (int k = 0, len = NoteGradients[i][j].Length; k != len; ++k) 182 | { 183 | // 根据渐变方向改变索引 184 | int idx = noteGradientDirection == HorizontalGradientDirection.FromLeftToRight ? k : (len - k - 1); 185 | cols[idx] = new RGBAColor((byte)r, (byte)g, (byte)b, gradientStart.A); 186 | if (Global.TranslucentNotes) // 如果是半透明音符, 那么调整颜色 187 | { 188 | ref RGBAColor c = ref cols[idx]; 189 | c.R = (byte)(background.R + Math.Round((c.R - background.R) * alphaRatio)); 190 | c.G = (byte)(background.G + Math.Round((c.G - background.G) * alphaRatio)); 191 | c.B = (byte)(background.B + Math.Round((c.B - background.B) * alphaRatio)); 192 | c.A = 0xFF; 193 | } 194 | 195 | if (brighterNotesOnHit) 196 | { 197 | // 进一步在已经得到的颜色上处理. 198 | BrightenedNoteGradients[i][j][idx] = RGBAColor.MixColors(cols[idx], RGBAColor.White, pressedNotesShadeDecrement); 199 | } 200 | 201 | r /= actualGradientRatio; 202 | g /= actualGradientRatio; 203 | b /= actualGradientRatio; 204 | } 205 | 206 | } 207 | } 208 | if (keyh == 0) 209 | { 210 | return; 211 | } 212 | double keyrgb = 255; 213 | referenceGradientRatio = Math.Pow(Math.Pow(Global.UnpressedWhiteKeyGradientScale, 154), 1.0 / UnpressedWhiteKeyGradients.Length); 214 | int yThreshold = keyh - (keyh * 64 / 100); 215 | int currentY = keyh / 20; 216 | if (keyboardGradientDirection == VerticalGradientDirection.FromButtomToTop) 217 | { 218 | for (int i = 0; i != UnpressedWhiteKeyGradients.Length; ++i) 219 | { 220 | UnpressedWhiteKeyGradients[i] = new RGBAColor((byte)keyrgb, (byte)keyrgb, (byte)keyrgb, 255); 221 | if (currentY < yThreshold) 222 | { 223 | keyrgb /= ((referenceGradientRatio - 1) / 3) + 1; 224 | } 225 | else 226 | { 227 | keyrgb /= referenceGradientRatio; 228 | } 229 | ++currentY; 230 | } 231 | } 232 | else 233 | { 234 | currentY = keyh; 235 | yThreshold = keyh - yThreshold; 236 | for (int i = UnpressedWhiteKeyGradients.Length - 1; i != -1; --i) 237 | { 238 | UnpressedWhiteKeyGradients[i] = new RGBAColor((byte)keyrgb, (byte)keyrgb, (byte)keyrgb, 255); 239 | if (currentY > yThreshold) 240 | { 241 | keyrgb /= ((referenceGradientRatio - 1) / 3) + 1; 242 | } 243 | else 244 | { 245 | keyrgb /= referenceGradientRatio; 246 | } 247 | --currentY; 248 | } 249 | } 250 | PressedWhiteKeyGradients = new RGBAColor[Global.KeyColors.Length][]; 251 | referenceGradientRatio = Math.Pow(Math.Pow(Global.PressedWhiteKeyGradientScale, 162), 1.0 / keyh); 252 | for (int i = 0; i != Global.KeyColors.Length; ++i) 253 | { 254 | PressedWhiteKeyGradients[i] = new RGBAColor[keyh]; 255 | RGBAColor[] cols = PressedWhiteKeyGradients[i]; 256 | RGBAColor gradientStart = Global.KeyColors[i]; 257 | double r = gradientStart.R, 258 | g = gradientStart.G, 259 | b = gradientStart.B; 260 | if (keyboardGradientDirection == VerticalGradientDirection.FromButtomToTop) 261 | { 262 | currentY = 0; 263 | for (int j = 0; j != keyh; ++j) 264 | { 265 | cols[j] = new RGBAColor((byte)r, (byte)g, (byte)b, gradientStart.A); 266 | if (currentY < yThreshold) 267 | { 268 | double gradientRatio = ((referenceGradientRatio - 1) / 1.2) + 1; 269 | r /= gradientRatio; 270 | g /= gradientRatio; 271 | b /= gradientRatio; 272 | } 273 | else 274 | { 275 | r /= referenceGradientRatio; 276 | g /= referenceGradientRatio; 277 | b /= referenceGradientRatio; 278 | } 279 | ++currentY; 280 | } 281 | } 282 | else 283 | { 284 | currentY = keyh; 285 | for (int j = keyh - 1; j != -1; --j) 286 | { 287 | cols[j] = new RGBAColor((byte)r, (byte)g, (byte)b, gradientStart.A); 288 | if (currentY > yThreshold) 289 | { 290 | double gradientRatio = ((referenceGradientRatio - 1) / 1.2) + 1; 291 | r /= gradientRatio; 292 | g /= gradientRatio; 293 | b /= gradientRatio; 294 | } 295 | else 296 | { 297 | r /= referenceGradientRatio; 298 | g /= referenceGradientRatio; 299 | b /= referenceGradientRatio; 300 | } 301 | --currentY; 302 | } 303 | } 304 | } 305 | } 306 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 307 | public int GetNoteX(int key) 308 | { 309 | return notex[key]; 310 | } 311 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 312 | public int GetKeyX(int key) 313 | { 314 | return keyx[key]; 315 | } 316 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 317 | public int GetNoteWidth(int key) 318 | { 319 | return notew[key]; 320 | } 321 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 322 | public int GetKeyWidth(int key) 323 | { 324 | return keyw[key]; 325 | } 326 | /// 327 | /// 绘制所有的琴键.
328 | /// Draw all keys. 329 | ///
330 | public void DrawKeys() 331 | { 332 | if (keyh == 0) 333 | { 334 | return; 335 | } 336 | int i, j; 337 | int bh = (whiteKeyShade || betterBlackKeys) ? keyh * 64 / 100 : keyh * 66 / 100; 338 | int bgr = keyh / 20; 339 | for (i = 0; i != 75; ++i) // 绘制所有白键. 参见 Global.DrawMap. Draws all white keys. 340 | { 341 | j = Global.DrawMap[i]; 342 | FillRectangle(keyx[j], 0, keyw[j], keyh, KeyColors[j]); 343 | DrawRectangle(keyx[j], 0, keyw[j] + 1, keyh, 0xFF000000); // 绘制琴键之间的分隔线. Draws a seperator between two keys. 344 | if (whiteKeyShade && !KeyPressed[j]) // 如果当前琴键未被按下 345 | { 346 | DrawRectangle(keyx[j], 0, keyw[j] + 1, bgr, 0xFF000000); 347 | FillRectangle(keyx[j] + 1, 1, keyw[j] - 1, bgr - 2, 0xFF999999); // 绘制琴键底部阴影. 感谢 Tweak 对阴影进行改善. 348 | } 349 | 350 | } 351 | int diff = keyh - bh; 352 | if (!betterBlackKeys) 353 | { 354 | for (; i != 128; ++i) // 绘制所有黑键. Draws all black keys. 355 | { 356 | j = Global.DrawMap[i]; 357 | FillRectangle(keyx[j], diff, keyw[j], bh, KeyColors[j]); // 重新绘制黑键及其颜色. Draws a black key (See Global.DrawMap). 358 | DrawRectangle(keyx[j], diff, keyw[j] + 1, bh, 0xFF000000); 359 | } 360 | } 361 | DrawSeperator(); 362 | if (betterBlackKeys) 363 | { 364 | int dtHeight = (int)Math.Round(keyh / 45.0); 365 | int dtWidth = (int)Math.Round(width / 1500.0); 366 | for (i = 75; i != 128; ++i) 367 | { 368 | j = Global.DrawMap[i]; 369 | if (KeyPressed[j]) 370 | { 371 | FillRectangle(keyx[j] - dtWidth, diff - dtWidth, keyw[j] + (2 * dtWidth), bh + dtWidth - 2, 0xFF363636); 372 | FillRectangle(keyx[j], diff, keyw[j], bh - 2, KeyColors[j]); 373 | } 374 | else 375 | { 376 | FillRectangle(keyx[j] - dtWidth, diff - dtWidth, keyw[j] + (2 * dtWidth), bh + dtWidth, 0xFF363636); 377 | FillRectangle(keyx[j], diff, keyw[j], bh + dtHeight, 0xFF000000); 378 | } 379 | } 380 | } 381 | } 382 | public void DrawGradientKeys() 383 | { 384 | if (keyh == 0) 385 | { 386 | return; 387 | } 388 | // bh: 黑键的高(不是坐标!) 389 | int i, j; 390 | int bh = (whiteKeyShade || betterBlackKeys) ? keyh * 64 / 100 : keyh * 66 / 100; 391 | int bgr = keyh / 20; 392 | for (i = 0; i != 75; ++i) // 先画白键 393 | { 394 | j = Global.DrawMap[i]; 395 | if (KeyPressed[j]) 396 | { 397 | //FillRectangle(keyx[j], 0, keyw[j], keyh, KeyColors[j]); 398 | for (int y = 0, yend = keyh; y != yend; ++y) 399 | { 400 | RGBAColor col = PressedWhiteKeyGradients[KeyTracks[j] % Global.KeyColors.Length][y]; 401 | for (int x = keyx[j], xend = x + keyw[j]; x != xend; ++x) 402 | { 403 | frameIdx[y][x] = col; 404 | } 405 | } 406 | } 407 | else 408 | { 409 | for (int y = whiteKeyShade ? bgr : 0, inity = y, yend = keyh; y != yend; ++y) 410 | { 411 | for (int x = keyx[j], xend = x + keyw[j]; x != xend; ++x) 412 | { 413 | frameIdx[y][x] = UnpressedWhiteKeyGradients[y - inity]; 414 | } 415 | } 416 | if (whiteKeyShade) 417 | { 418 | DrawRectangle(keyx[j], 0, keyw[j] + 1, bgr, 0xFF000000); 419 | FillRectangle(keyx[j] + 1, 1, keyw[j] - 1, bgr - 2, 0xFF999999); 420 | } 421 | } 422 | 423 | DrawRectangle(keyx[j], 0, keyw[j] + 1, keyh, 0xFF000000); 424 | } 425 | int diff = keyh - bh; 426 | if (!betterBlackKeys) 427 | { 428 | for (; i != 128; ++i) // 绘制所有黑键. Draws all black keys. 429 | { 430 | j = Global.DrawMap[i]; 431 | FillRectangle(keyx[j], diff, keyw[j], bh, KeyColors[j]); // 重新绘制黑键及其颜色. Draws a black key (See Global.DrawMap). 432 | DrawRectangle(keyx[j], diff, keyw[j] + 1, bh, 0xFF000000); 433 | } 434 | } 435 | DrawSeperator(); 436 | if (betterBlackKeys) 437 | { 438 | int dtHeight = (int)Math.Round(keyh / 45.0); 439 | int dtWidth = (int)Math.Round(width / 1500.0); 440 | for (i = 75; i != 128; ++i) 441 | { 442 | j = Global.DrawMap[i]; 443 | if (KeyPressed[j]) 444 | { 445 | FillRectangle(keyx[j] - dtWidth, diff - dtWidth, keyw[j] + (2 * dtWidth), bh + dtWidth - 2, 0xFF363636); 446 | FillRectangle(keyx[j], diff, keyw[j], bh - 2, KeyColors[j]); 447 | } 448 | else 449 | { 450 | FillRectangle(keyx[j] - dtWidth, diff - dtWidth, keyw[j] + (2 * dtWidth), bh + dtWidth, 0xFF363636); 451 | FillRectangle(keyx[j], diff, keyw[j], bh + dtHeight, 0xFF000000); // 重新绘制黑键及其颜色. Draws a black key (See Global.DrawMap). 452 | } 453 | } 454 | } 455 | //FillRectangle(0, keyh - 2, width, keyh / 15, lineColor); 456 | } 457 | public new void Dispose() 458 | { 459 | base.Dispose(); 460 | GC.SuppressFinalize(this); 461 | } 462 | ~CommonCanvas() 463 | { 464 | Dispose(); 465 | } 466 | /// 467 | /// 绘制一个音符.
468 | /// Draws a note. 469 | ///
470 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 471 | public void DrawNote(short key, int colorIndex, int y, int height, uint noteColor, bool pressed) 472 | { 473 | if (height < 1) 474 | { 475 | height = 1; 476 | } 477 | if (brighterNotesOnHit && pressed) 478 | { 479 | noteColor = RGBAColor.MixColors(noteColor, RGBAColor.White, pressedNotesShadeDecrement); 480 | } 481 | if (Global.EnableNoteBorder) 482 | { 483 | DrawBorderedNote(key, colorIndex, y, height, noteColor); 484 | } 485 | else 486 | { 487 | if (height > 5) 488 | { 489 | --height; 490 | } 491 | FillRectangle(notex[key] + 1, y, notew[key] - 1, height, noteColor); 492 | } 493 | } 494 | private void DrawBorderedNote(short key, int colorIndex, int y, int height, uint noteColor) 495 | { 496 | RGBAColor borderColor = BorderColors[colorIndex]; 497 | if (height > 2 * borderHeight) 498 | { 499 | FillRectangle(notex[key], y, notew[key], height, borderColor); 500 | if (y + height != this.height) 501 | { 502 | FillRectangle(notex[key] + borderWidth, y + borderHeight, notew[key] - (borderWidth * 2), height - (borderHeight * 2), noteColor); 503 | } 504 | else 505 | { 506 | FillRectangle(notex[key] + borderWidth, y + borderHeight, notew[key] - (borderWidth * 2), height - borderHeight, noteColor); 507 | } 508 | } 509 | else 510 | { 511 | if (Global.EnableDenseNoteEffect) 512 | { 513 | FillRectangle(notex[key], y, notew[key], height, DenseNoteColors[colorIndex]); 514 | } 515 | else 516 | { 517 | FillRectangle(notex[key], y, notew[key], height, borderColor); 518 | FillRectangle(notex[key] + borderWidth, y, notew[key] - (borderWidth * 2), height, noteColor); 519 | } 520 | } 521 | } 522 | private void DrawGradientBorderedNote(short key, int colorIndex, int y, int height, bool pressed) 523 | { 524 | RGBAColor[] gradientColors = (pressed && brighterNotesOnHit) ? BrightenedNoteGradients[key][colorIndex] : NoteGradients[key][colorIndex]; 525 | RGBAColor borderColor = Global.KeyColors[colorIndex]; 526 | borderColor.R = (byte)(borderColor.R / Global.NoteBorderShade); 527 | borderColor.G = (byte)(borderColor.G / Global.NoteBorderShade); 528 | borderColor.B = (byte)(borderColor.B / Global.NoteBorderShade); 529 | if (height > 2 * borderHeight) 530 | { 531 | if (y + height != this.height) 532 | { 533 | FillRectangle(notex[key], y, notew[key], height, borderColor); 534 | for (int x = notex[key] + borderWidth, xend = x + notew[key] - (2 * borderWidth), initx = x; x != xend; ++x) 535 | { 536 | uint col = gradientColors[x - initx]; 537 | for (int dy = y + borderHeight, yend = y + height - borderHeight; dy != yend; ++dy) 538 | { 539 | frameIdx[dy][x] = col; 540 | } 541 | } 542 | } 543 | else 544 | { 545 | FillRectangle(notex[key], y, notew[key], height, borderColor); 546 | for (int x = notex[key] + borderWidth, xend = x + notew[key] - (2 * borderWidth), initx = x; x != xend; ++x) 547 | { 548 | uint col = gradientColors[x - initx]; 549 | for (int dy = y + borderHeight, yend = y + height; dy != yend; ++dy) 550 | { 551 | frameIdx[dy][x] = col; 552 | } 553 | } 554 | } 555 | } 556 | else 557 | { 558 | if (Global.EnableDenseNoteEffect) // 不必要做变白的处理, 因为效果并不明显 559 | { 560 | FillRectangle(notex[key], y, notew[key], height, DenseNoteColors[colorIndex]); 561 | } 562 | else 563 | { 564 | FillRectangle(notex[key], y, notew[key], height, borderColor); 565 | for (int x = notex[key] + borderWidth, xend = x + notew[key] - (2 * borderWidth), initx = x; x != xend; ++x) 566 | { 567 | uint col = gradientColors[x - initx]; 568 | for (int dy = y, yend = dy + height; dy != yend; ++dy) 569 | { 570 | frameIdx[dy][x] = col; 571 | } 572 | } 573 | } 574 | } 575 | } 576 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 577 | public void DrawGradientNote(short key, int colorIndex, int y, int height, bool pressed) 578 | { 579 | if (height < 1) 580 | { 581 | height = 1; 582 | } 583 | DrawGradientBorderedNote(key, colorIndex, y, height, pressed); 584 | } 585 | 586 | private void DrawSeperator() 587 | { 588 | if (!separator) 589 | { 590 | return; 591 | } 592 | if (enableGradient) 593 | { 594 | RGBAColor gradientStart = lineColor; 595 | double r = gradientStart.R, 596 | g = gradientStart.G, 597 | b = gradientStart.B; 598 | double gradientScale = Math.Pow(Math.Pow(Global.SeparatorGradientScale, 162 / 15), 1.0 / (keyh / 15)); 599 | if (separatorGradientDirection == VerticalGradientDirection.FromButtomToTop) 600 | { 601 | for (int y = keyh - 2, yend = y + (keyh / 15); y != yend; ++y) 602 | { 603 | uint col = (uint)((byte)r | ((byte)g << 8) | ((byte)b << 16) | (gradientStart.A << 24)); 604 | for (int x = 0; x != width; ++x) 605 | { 606 | frameIdx[y][x] = col; 607 | } 608 | r /= gradientScale; 609 | g /= gradientScale; 610 | b /= gradientScale; 611 | } 612 | } 613 | else 614 | { 615 | for (int y = keyh - 3 + (keyh / 15), yend = keyh - 3; y != yend; --y) 616 | { 617 | uint col = (uint)((byte)r | ((byte)g << 8) | ((byte)b << 16) | (gradientStart.A << 24)); 618 | for (int x = 0; x != width; ++x) 619 | { 620 | frameIdx[y][x] = col; 621 | } 622 | r /= gradientScale; 623 | g /= gradientScale; 624 | b /= gradientScale; 625 | } 626 | } 627 | } 628 | else 629 | { 630 | FillRectangle(0, keyh - 2, width, keyh / 15, lineColor); 631 | } 632 | } 633 | } 634 | } 635 | --------------------------------------------------------------------------------