From ab8ae33e619eb151c26934ab60873dcc55a5b7de Mon Sep 17 00:00:00 2001 From: logchan Date: Fri, 18 Nov 2016 15:10:05 +0800 Subject: [PATCH 01/13] Change render method Remove all UIElements Use Canvas.OnRender to draw --- ...Tore.Applications.StarlightDirector.csproj | 1 + .../UI/Controls/PreviewCanvas.cs | 138 +++++++++ .../UI/Controls/ScorePreviewer.xaml | 3 +- .../UI/Controls/ScorePreviewer.xaml.cs | 291 ++++-------------- 4 files changed, 198 insertions(+), 235 deletions(-) create mode 100644 DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs diff --git a/DereTore.Applications.StarlightDirector/DereTore.Applications.StarlightDirector.csproj b/DereTore.Applications.StarlightDirector/DereTore.Applications.StarlightDirector.csproj index 4d6ecbf..0c5160b 100644 --- a/DereTore.Applications.StarlightDirector/DereTore.Applications.StarlightDirector.csproj +++ b/DereTore.Applications.StarlightDirector/DereTore.Applications.StarlightDirector.csproj @@ -203,6 +203,7 @@ + LineLayer.xaml diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs b/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs new file mode 100644 index 0000000..0cf5769 --- /dev/null +++ b/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using DereTore.Applications.StarlightDirector.Extensions; + +namespace DereTore.Applications.StarlightDirector.UI.Controls +{ + public class PreviewCanvas : Canvas + { + private List _hitEffectCountdown; + + public int HitEffectFrames { get; set; } + + public EventWaitHandle RenderCompleteHandle { get; } = new EventWaitHandle(false, EventResetMode.AutoReset); + + // Type, x, y + // Types: 0 - tap, 1 - left, 2 - right, 3 - hold + public List> Notes { get; } = new List>(); + + // Type, x1, y1, x2, y2 + // Types: 0 - hold, 1 - sync, 2 - group + public List> Lines { get; } = new List>(); + + public PreviewCanvas() + { + _hitEffectCountdown = new List(); + for (int i = 0; i < 5; ++i) + { + _hitEffectCountdown.Add(0); + } + } + + public void NoteHit(int position) + { + _hitEffectCountdown[position] = HitEffectFrames; + } + + #region Brushes and Pens + + private static readonly SolidColorBrush NoteStroke = + new SolidColorBrush(Color.FromRgb(0x22, 0x22, 0x22)); + + private static readonly SolidColorBrush NoteShapeOutlineFill = Brushes.White; + + private static readonly SolidColorBrush NormalNoteShapeStroke = + new SolidColorBrush(Color.FromRgb(0xFF, 0x33, 0x66)); + + private static readonly LinearGradientBrush NormalNoteShapeFill = + new LinearGradientBrush(Color.FromRgb(0xFF, 0x33, 0x66), Color.FromRgb(0xFF, 0x99, 0xBB), 90.0); + + private static readonly SolidColorBrush HoldNoteShapeStroke = + new SolidColorBrush(Color.FromRgb(0xFF, 0xBB, 0x22)); + + private static readonly LinearGradientBrush HoldNoteShapeFillOuter = + new LinearGradientBrush(Color.FromRgb(0xFF, 0xBB, 0x22), Color.FromRgb(0xFF, 0xDD, 0x66), 90.0); + + private static readonly SolidColorBrush HoldNoteShapeFillInner = Brushes.White; + + private static readonly SolidColorBrush FlickNoteShapeStroke = + new SolidColorBrush(Color.FromRgb(0x22, 0x55, 0xBB)); + + private static readonly LinearGradientBrush FlickNoteShapeFillOuter = + new LinearGradientBrush(Color.FromRgb(0x22, 0x55, 0xBB), Color.FromRgb(0x88, 0xBB, 0xFF), 90.0); + + private static readonly SolidColorBrush FlickNoteShapeFillInner = Brushes.White; + + private static readonly Pen NoteStrokePen = new Pen(NoteStroke, 1.5); + + private static readonly Pen NormalNoteShapeStrokePen = new Pen(NormalNoteShapeStroke, 1); + + private static readonly Pen HoldNoteShapeStrokePen = new Pen(HoldNoteShapeStroke, 1); + + private static readonly Pen FlickNoteShapeStrokePen = new Pen(FlickNoteShapeStroke, 1); + + private static readonly Brush RelationBrush = Application.Current.FindResource(App.ResourceKeys.RelationBorderBrush); + + private static readonly Pen[] LinePens = + { + new Pen(RelationBrush, 16), // hold line + new Pen(RelationBrush, 2), // sync line + new Pen(RelationBrush, 8) // group line + }; + + #endregion + + #region Position Computation + + + + #endregion + + protected override void OnRender(DrawingContext dc) + { + base.OnRender(dc); + + // draw lines + + foreach (var line in Lines) + { + var p1 = new Point(line.Item2, line.Item3); + var p2 = new Point(line.Item4, line.Item5); + + dc.DrawLine(LinePens[line.Item1], p1, p2); + } + + // draw notes + + foreach (var note in Notes) + { + var x = note.Item2; + var y = note.Item3; + var center = new Point(x, y); + + switch (note.Item1) + { + case 0: + case 1: + case 2: + dc.DrawEllipse(NoteShapeOutlineFill, NoteStrokePen, center, 15, 15); + dc.DrawEllipse(NormalNoteShapeFill, NormalNoteShapeStrokePen, center, 11, 11); + break; + case 3: + dc.DrawEllipse(NoteShapeOutlineFill, NoteStrokePen, center, 15, 15); + dc.DrawEllipse(HoldNoteShapeFillOuter, HoldNoteShapeStrokePen, center, 11, 11); + dc.DrawEllipse(HoldNoteShapeFillInner, null, center, 5, 5); + break; + } + } + + RenderCompleteHandle.Set(); + } + } +} diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml b/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml index 8c0db31..92ec8f9 100644 --- a/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml +++ b/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml @@ -7,7 +7,6 @@ mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300" Background="Transparent"> - - + diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs b/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs index d584a5d..6286490 100644 --- a/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs +++ b/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs @@ -12,7 +12,7 @@ using DereTore.Applications.StarlightDirector.UI.Controls.Primitives; using System.Linq; using DereTore.Applications.StarlightDirector.UI.Windows; -using LineTuple = System.Tuple; +using LineTuple = System.Tuple; namespace DereTore.Applications.StarlightDirector.UI.Controls { @@ -38,10 +38,8 @@ private class SimpleNote public double LastT { get; set; } public double X { get; set; } public double Y { get; set; } - public SimpleScoreNote ScoreNote { get; set; } - public Line HoldLine { get; set; } - public Line GroupLine { get; set; } - public Line SyncLine { get; set; } + public int DrawType { get; set; } + public int HitPosition { get; set; } } private const double NoteMarginTop = 20; @@ -49,9 +47,6 @@ private class SimpleNote private const double NoteXBetween = 80; private const double NoteSize = 30; private const double NoteRadius = NoteSize / 2; - private const double HoldLineThickness = 16; - private const double SyncLineThickness = 8; - private const double GridLineThickness = 1; // used by frame update thread private Score _score; @@ -62,28 +57,15 @@ private class SimpleNote private int _startTime; private double _approachTime; - // WPF new object performance is horrible, so we use pools to reuse them - private readonly Queue _scoreNotePool = new Queue(); - private readonly Queue _linePool = new Queue(); - // screen positions private double _noteStartY; private double _noteEndY; private List _noteX; - private double _lineOffset; - - private readonly Brush _relationBrush = Application.Current.FindResource(App.ResourceKeys.RelationBorderBrush); - private readonly Brush _gridBrush = Brushes.DarkGray; - + // window-related private readonly MainWindow _window; private bool _shouldPlayMusic; - // note hit effect! - private int _noteHitCounter; - private int _noteHitMax; - private Line _effectedLine; - public ScorePreviewer() { InitializeComponent(); @@ -129,9 +111,12 @@ public void BeginPreview(Score score, double targetFps, int startTime, double ap Timing = (int) (note.HitTiming*1000), LastT = 0, X = _noteX[pos - 1], - Y = _noteStartY + Y = _noteStartY, + HitPosition = pos - 1 }; + snote.DrawType = note.Type == NoteType.Hold ? 3 : (int) note.FlickType; + if (note.IsHoldStart) { snote.Duration = (int) (note.HoldTarget.HitTiming*1000) - (int) (note.HitTiming*1000); @@ -161,9 +146,6 @@ public void BeginPreview(Score score, double targetFps, int startTime, double ap } } - // draw some grid lines - DrawGrid(); - // music _shouldPlayMusic = ShouldPlayMusic(); @@ -187,45 +169,12 @@ public void EndPreview() private void ComputePositions() { _noteStartY = NoteMarginTop; - _noteEndY = PreviewCanvas.ActualHeight - NoteMarginBottom; - _noteX[0] = (PreviewCanvas.ActualWidth - 4*NoteXBetween - 5*NoteSize)/2; + _noteEndY = MainCanvas.ActualHeight - NoteMarginBottom; + _noteX[0] = (MainCanvas.ActualWidth - 4*NoteXBetween - 5*NoteSize)/2; for (int i = 1; i < 5; ++i) { _noteX[i] = _noteX[i - 1] + NoteXBetween + NoteSize; } - - _lineOffset = NoteRadius; - } - - private void DrawGrid() - { - var lines = new List(); - - foreach (var x in _noteX) - { - lines.Add(new LineTuple(x, _noteStartY, x, _noteEndY)); - } - lines.Add(new LineTuple(_noteX[0], _noteStartY, _noteX[4], _noteStartY)); - lines.Add(new LineTuple(_noteX[0], _noteEndY, _noteX[4], _noteEndY)); - lines.Add(new LineTuple(_noteX[0], _noteEndY, _noteX[4], _noteEndY)); - - foreach (var line in lines) - { - // after the loop, _effectedLine will be the LAST line - _effectedLine = new Line - { - Stroke = _gridBrush, - StrokeThickness = GridLineThickness, - X1 = line.Item1 + _lineOffset, - Y1 = line.Item2 + _lineOffset, - X2 = line.Item3 + _lineOffset, - Y2 = line.Item4 + _lineOffset - }; - LineCanvas.Children.Add(_effectedLine); - } - - _effectedLine.Stroke = Brushes.Gold; - _effectedLine.Opacity = 0; } // These methods invokes the main thread and perform the tasks @@ -246,150 +195,31 @@ private void StopMusic() Dispatcher.Invoke(new Action(() => _window.StopMusic())); } - private readonly Action _setPositionInCanvasAction = - (elem, x, y) => - { - Canvas.SetLeft(elem, x); - Canvas.SetTop(elem, y); - }; - - private SimpleScoreNote CreateScoreNote(SimpleNote note) - { - return Dispatcher.Invoke(new Func(() => - { - if (_scoreNotePool.Count == 0) - { - var scoreNote = new SimpleScoreNote {Note = note.Note}; - PreviewCanvas.Children.Add(scoreNote); - Canvas.SetTop(scoreNote, 0); - Canvas.SetLeft(scoreNote, 0); - return scoreNote; - } - else - { - var scoreNote = _scoreNotePool.Dequeue(); - scoreNote.Note = note.Note; - Canvas.SetTop(scoreNote, 0); - Canvas.SetLeft(scoreNote, 0); - scoreNote.Visibility = Visibility.Visible; - return scoreNote; - } - })) as SimpleScoreNote; - } - - private void SetPositionInCanvas(UIElement elem, double x, double y) - { - Dispatcher.Invoke(_setPositionInCanvasAction, elem, x, y); - } - - private void ReleaseScoreNote(SimpleScoreNote scoreNote) - { - Dispatcher.Invoke(new Action(() => - { - scoreNote.Visibility = Visibility.Collapsed; - _scoreNotePool.Enqueue(scoreNote); - })); - } + #endregion - private void ReleaseLine(Line line) + private void ComputeLine(SimpleNote note) { - if (line == null) + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (note.LastT == 0 || note.Done) return; - Dispatcher.Invoke(new Action(() => - { - line.Visibility = Visibility.Collapsed; - _linePool.Enqueue(line); - })); - } - - private void ClearCanvases() - { - Dispatcher.Invoke(new Action(() => - { - PreviewCanvas.Children.Clear(); - LineCanvas.Children.Clear(); - })); - } - - #endregion - - /// - /// Helper used by DrawLines. - /// - private Line CreateLineOnCurrentThread(double thickness) - { - Line line; - if (_linePool.Count == 0) + // Hold line + if (note.IsHoldStart) { - line = new Line - { - Stroke = _relationBrush, - StrokeThickness = thickness - }; - LineCanvas.Children.Add(line); + MainCanvas.Lines.Add(new LineTuple(0, note.X, note.Y, note.HoldTarget.X, note.HoldTarget.Y)); } - else + + // Sync line + // check LastT so that when HitNote arrives the line is gone + if (note.SyncTarget != null && note.LastT < 1) { - line = _linePool.Dequeue(); - line.Visibility = Visibility.Visible; - line.StrokeThickness = thickness; + MainCanvas.Lines.Add(new LineTuple(1, note.X, note.Y, note.SyncTarget.X, note.SyncTarget.Y)); } - return line; - } - - /// - /// Draw lines. MUST BE CALLED ON MAIN THREAD - /// - private void DrawLines() - { - foreach (var note in _notes) + // Flicker line + if (note.GroupTarget != null && note.LastT < 1) { - // ReSharper disable once CompareOfFloatsByEqualityOperator - if (note.LastT == 0 || note.Done) - continue; - - // Hold line - if (note.IsHoldStart) - { - if (note.HoldLine == null) - { - note.HoldLine = CreateLineOnCurrentThread(HoldLineThickness); - } - - note.HoldLine.X1 = note.HoldLine.X2 = note.X + _lineOffset; - note.HoldLine.Y1 = note.Y + _lineOffset; - note.HoldLine.Y2 = note.HoldTarget.Y + _lineOffset; - } - - // Sync line - // check LastT so that when HitNote arrives the line is gone - if (note.SyncTarget != null && note.LastT < 1) - { - if (note.SyncLine == null) - { - note.SyncLine = CreateLineOnCurrentThread(SyncLineThickness); - } - - note.SyncLine.X1 = note.X + _lineOffset; - note.SyncLine.Y1 = note.SyncLine.Y2 = note.Y + _lineOffset; - note.SyncLine.X2 = note.SyncTarget.X + _lineOffset; - } - - // Flicker line - if (note.GroupTarget != null && note.LastT < 1) - { - if (note.GroupLine == null) - { - note.GroupLine = CreateLineOnCurrentThread(SyncLineThickness); - } - - note.GroupLine.X1 = note.X + _lineOffset; - note.GroupLine.Y1 = note.Y + _lineOffset; - note.GroupLine.X2 = note.GroupTarget.X + _lineOffset; - note.GroupLine.Y2 = note.GroupTarget.Y + _lineOffset; - } + MainCanvas.Lines.Add(new LineTuple(2, note.X, note.Y, note.GroupTarget.X, note.GroupTarget.Y)); } } @@ -400,7 +230,7 @@ private void DrawPreviewFrame() { // computation parameters var targetFrameTime = 1000 / _targetFps; - _noteHitMax = (int) (_targetFps / 5); // effect lasts for 0.2 second + MainCanvas.HitEffectFrames = (int) (_targetFps / 5); // effect lasts for 0.2 second // fix start time _startTime -= (int)_approachTime; @@ -420,16 +250,14 @@ private void DrawPreviewFrame() { if (!_isPreviewing) { - ClearCanvases(); - - _linePool.Clear(); _notes.Clear(); - _scoreNotePool.Clear(); return; } var frameStartTime = DateTime.UtcNow; + // compute time + int songTime; if (_shouldPlayMusic) { @@ -440,6 +268,15 @@ private void DrawPreviewFrame() songTime = (int)(frameStartTime - startTime).TotalMilliseconds + _startTime; } + // reset canvas + + MainCanvas.Notes.Clear(); + MainCanvas.Lines.Clear(); + + // compute notes + + int computeStart = notesHead; + int computeEnd = notesHead; bool headUpdated = false; for (int i = notesHead; i < _notes.Count; ++i) { @@ -457,10 +294,7 @@ private void DrawPreviewFrame() if (note.Done) continue; - if (note.ScoreNote == null) - { - note.ScoreNote = CreateScoreNote(note); - } + computeEnd = i; var t = diff/ _approachTime; if (t > 1) @@ -470,53 +304,44 @@ private void DrawPreviewFrame() // change this line if you want game-like approaching path note.Y = _noteStartY + t*(_noteEndY - _noteStartY); - SetPositionInCanvas(note.ScoreNote, note.X, note.Y); + + MainCanvas.Notes.Add(new Tuple(note.DrawType, note.X, note.Y)); // note arrive at bottom if (diff > _approachTime) { - ReleaseLine(note.SyncLine); - ReleaseLine(note.GroupLine); - note.SyncLine = null; - note.GroupLine = null; + MainCanvas.NoteHit(note.HitPosition); // Hit and flick notes end immediately // Hold note heads end after its duration if (!note.IsHoldStart || diff > _approachTime + note.Duration) { - ReleaseScoreNote(note.ScoreNote); - ReleaseLine(note.HoldLine); - note.ScoreNote = null; - note.HoldLine = null; - note.Done = true; + } + } - // note hit effect - _noteHitCounter = _noteHitMax; - } - - if (!headUpdated) - { - notesHead = i; - headUpdated = true; - } + // update head to be the first note that is not done + if (!note.Done && !headUpdated) + { + notesHead = i; + headUpdated = true; } } - // Draw sync, group, hold lines - Dispatcher.Invoke(new Action(DrawLines)); + // compute lines - // Do note hit effect - if (_noteHitCounter >= 0) + for (int i = computeStart; i <= computeEnd; ++i) { - Dispatcher.Invoke(new Action(() => - { - double et = _noteHitCounter/(double) _noteHitMax; // from 1 to 0 - _effectedLine.Opacity = et; - })); - --_noteHitCounter; + ComputeLine(_notes[i]); } + // wait for rendering + + Dispatcher.Invoke(new Action(() => MainCanvas.InvalidateVisual())); + MainCanvas.RenderCompleteHandle.WaitOne(); + + // wait for next frame + var frameEllapsedTime = (DateTime.UtcNow - frameStartTime).TotalMilliseconds; if (frameEllapsedTime < targetFrameTime) { From 6a622d1f7b0c237c073c7390f13bb0d0b887285a Mon Sep 17 00:00:00 2001 From: logchan Date: Fri, 18 Nov 2016 15:11:43 +0800 Subject: [PATCH 02/13] Remove unused primitive --- ...Tore.Applications.StarlightDirector.csproj | 7 --- .../Controls/Primitives/SimpleScoreNote.xaml | 61 ------------------- .../Primitives/SimpleScoreNote.xaml.cs | 36 ----------- 3 files changed, 104 deletions(-) delete mode 100644 DereTore.Applications.StarlightDirector/UI/Controls/Primitives/SimpleScoreNote.xaml delete mode 100644 DereTore.Applications.StarlightDirector/UI/Controls/Primitives/SimpleScoreNote.xaml.cs diff --git a/DereTore.Applications.StarlightDirector/DereTore.Applications.StarlightDirector.csproj b/DereTore.Applications.StarlightDirector/DereTore.Applications.StarlightDirector.csproj index 0c5160b..2149e05 100644 --- a/DereTore.Applications.StarlightDirector/DereTore.Applications.StarlightDirector.csproj +++ b/DereTore.Applications.StarlightDirector/DereTore.Applications.StarlightDirector.csproj @@ -158,10 +158,6 @@ Designer MSBuild:Compile - - Designer - MSBuild:Compile - Designer MSBuild:Compile @@ -236,9 +232,6 @@ - - SimpleScoreNote.xaml - diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/Primitives/SimpleScoreNote.xaml b/DereTore.Applications.StarlightDirector/UI/Controls/Primitives/SimpleScoreNote.xaml deleted file mode 100644 index 54d8da1..0000000 --- a/DereTore.Applications.StarlightDirector/UI/Controls/Primitives/SimpleScoreNote.xaml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/Primitives/SimpleScoreNote.xaml.cs b/DereTore.Applications.StarlightDirector/UI/Controls/Primitives/SimpleScoreNote.xaml.cs deleted file mode 100644 index e9ceb87..0000000 --- a/DereTore.Applications.StarlightDirector/UI/Controls/Primitives/SimpleScoreNote.xaml.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; -using DereTore.Applications.StarlightDirector.Entities; - -namespace DereTore.Applications.StarlightDirector.UI.Controls.Primitives -{ - /// - /// Interaction logic for SimpleScoreNote.xaml - /// - public partial class SimpleScoreNote : UserControl - { - public static readonly DependencyProperty NoteProperty = DependencyProperty.Register("Note", typeof(Note), typeof(SimpleScoreNote), new PropertyMetadata(default(Note))); - - public SimpleScoreNote() - { - InitializeComponent(); - } - - public Note Note - { - get { return (Note) GetValue(NoteProperty); } - set { SetValue(NoteProperty, value); } - } - } -} From a5675d57432d1dd920cc8fa6923047adba4acc7c Mon Sep 17 00:00:00 2001 From: logchan Date: Fri, 18 Nov 2016 16:15:48 +0800 Subject: [PATCH 03/13] Move computation and positioning to PreviewCanvas --- ...Tore.Applications.StarlightDirector.csproj | 1 + .../UI/Controls/Models/DrawingNote.cs | 28 +++ .../UI/Controls/PreviewCanvas.cs | 209 +++++++++++++++--- .../UI/Controls/ScorePreviewer.xaml.cs | 209 +++--------------- 4 files changed, 238 insertions(+), 209 deletions(-) create mode 100644 DereTore.Applications.StarlightDirector/UI/Controls/Models/DrawingNote.cs diff --git a/DereTore.Applications.StarlightDirector/DereTore.Applications.StarlightDirector.csproj b/DereTore.Applications.StarlightDirector/DereTore.Applications.StarlightDirector.csproj index 2149e05..01308c9 100644 --- a/DereTore.Applications.StarlightDirector/DereTore.Applications.StarlightDirector.csproj +++ b/DereTore.Applications.StarlightDirector/DereTore.Applications.StarlightDirector.csproj @@ -199,6 +199,7 @@ + diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/Models/DrawingNote.cs b/DereTore.Applications.StarlightDirector/UI/Controls/Models/DrawingNote.cs new file mode 100644 index 0000000..04203a0 --- /dev/null +++ b/DereTore.Applications.StarlightDirector/UI/Controls/Models/DrawingNote.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using DereTore.Applications.StarlightDirector.Entities; + +namespace DereTore.Applications.StarlightDirector.UI.Controls.Models +{ + /// + /// Internal representation of notes for drawing + /// + public class DrawingNote + { + public Note Note { get; set; } + public DrawingNote HoldTarget { get; set; } + public DrawingNote SyncTarget { get; set; } + public DrawingNote GroupTarget { get; set; } + public int Timing { get; set; } + public bool Done { get; set; } + public int Duration { get; set; } + public bool IsHoldStart { get; set; } + public double LastT { get; set; } + public double X { get; set; } + public double Y { get; set; } + public int DrawType { get; set; } + public int HitPosition { get; set; } + } +} diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs b/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs index 0cf5769..3ef7be6 100644 --- a/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs +++ b/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs @@ -7,25 +7,37 @@ using System.Windows.Controls; using System.Windows.Media; using DereTore.Applications.StarlightDirector.Extensions; +using DereTore.Applications.StarlightDirector.UI.Controls.Models; namespace DereTore.Applications.StarlightDirector.UI.Controls { public class PreviewCanvas : Canvas { - private List _hitEffectCountdown; + // constants + private const double NoteMarginTop = 20; + private const double NoteMarginBottom = 60; + private const double NoteXBetween = 80; + private const double NoteSize = 30; + private const double NoteRadius = NoteSize / 2; + + // dimensions + private double _noteStartY; + private double _noteEndY; + private readonly List _noteX = new List(); + + // computation + private List _notes; + private int _notesHead; + private int _notesTail; + private double _approachTime; + + // rendering + private volatile bool _isPreviewing; + private readonly List _hitEffectCountdown; + private readonly EventWaitHandle _renderCompleteHandle = new EventWaitHandle(false, EventResetMode.AutoReset); public int HitEffectFrames { get; set; } - public EventWaitHandle RenderCompleteHandle { get; } = new EventWaitHandle(false, EventResetMode.AutoReset); - - // Type, x, y - // Types: 0 - tap, 1 - left, 2 - right, 3 - hold - public List> Notes { get; } = new List>(); - - // Type, x1, y1, x2, y2 - // Types: 0 - hold, 1 - sync, 2 - group - public List> Lines { get; } = new List>(); - public PreviewCanvas() { _hitEffectCountdown = new List(); @@ -35,6 +47,48 @@ public PreviewCanvas() } } + public void Initialize(List notes, double approachTime) + { + _notes = notes; + _approachTime = approachTime; + _notesHead = 0; + + // compute positions + + _noteStartY = NoteMarginTop; + _noteEndY = ActualHeight - NoteMarginBottom; + + _noteX.Clear(); + _noteX.Add((ActualWidth - 4 * NoteXBetween - 5 * NoteSize) / 2); + for (int i = 1; i < 5; ++i) + { + _noteX.Add(_noteX.Last() + NoteXBetween + NoteSize); + } + + // initialize note positions + + foreach (var note in _notes) + { + note.X = _noteX[note.HitPosition]; + note.Y = _noteStartY; + } + + _isPreviewing = true; + } + + public void RenderFrameBlocked(int songTime) + { + ComputeFrame(songTime); + + Dispatcher.Invoke(new Action(InvalidateVisual)); + _renderCompleteHandle.WaitOne(); + } + + public void Stop() + { + _isPreviewing = false; + } + public void NoteHit(int position) { _hitEffectCountdown[position] = HitEffectFrames; @@ -88,51 +142,140 @@ public void NoteHit(int position) #endregion - #region Position Computation + #region Computation and Positions + private void SetNotePosition(DrawingNote note, double t) + { + note.Y = _noteStartY + t * (_noteEndY - _noteStartY); + } + private void ComputeFrame(int songTime) + { + var headUpdated = false; + for (int i = _notesHead; i < _notes.Count; ++i) + { + var note = _notes[i]; + + /* + * diff < 0 --- not shown + * diff == 0 --- begin to show + * diff == approachTime --- arrive at end + * diff > approachTime --- ended + */ + var diff = songTime - note.Timing + _approachTime; + if (diff < 0) + break; + if (note.Done) + continue; + + _notesTail = i; + + var t = diff / _approachTime; + if (t > 1) + t = 1; + + note.LastT = t; + SetNotePosition(note, t); + + // note arrive at bottom + if (diff > _approachTime) + { + NoteHit(note.HitPosition); + + // Hit and flick notes end immediately + // Hold note heads end after its duration + if (!note.IsHoldStart || diff > _approachTime + note.Duration) + { + note.Done = true; + } + } + + // update head to be the first note that is not done + if (!note.Done && !headUpdated) + { + _notesHead = i; + headUpdated = true; + } + } + } #endregion - protected override void OnRender(DrawingContext dc) + #region Rendering + + private void DrawLines(DrawingContext dc) { - base.OnRender(dc); + for (int i = _notesHead; i <= _notesTail; ++i) + { + var note = _notes[i]; - // draw lines + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (note.LastT == 0 || note.Done) + return; - foreach (var line in Lines) - { - var p1 = new Point(line.Item2, line.Item3); - var p2 = new Point(line.Item4, line.Item5); + // Hold line + if (note.IsHoldStart) + { + dc.DrawLine(LinePens[0], new Point(note.X, note.Y), new Point(note.HoldTarget.X, note.HoldTarget.Y)); + } - dc.DrawLine(LinePens[line.Item1], p1, p2); - } + // Sync line + // check LastT so that when HitNote arrives the line is gone + if (note.SyncTarget != null && note.LastT < 1) + { + dc.DrawLine(LinePens[1], new Point(note.X, note.Y), new Point(note.SyncTarget.X, note.SyncTarget.Y)); + } - // draw notes + // Flicker line + if (note.GroupTarget != null && note.LastT < 1) + { + dc.DrawLine(LinePens[2], new Point(note.X, note.Y), new Point(note.GroupTarget.X, note.GroupTarget.Y)); + } + } + } - foreach (var note in Notes) + private void DrawNotes(DrawingContext dc) + { + for (int i = _notesHead; i <= _notesTail; ++i) { - var x = note.Item2; - var y = note.Item3; - var center = new Point(x, y); + var note = _notes[i]; + var center = new Point(note.X, note.Y); - switch (note.Item1) + switch (note.DrawType) { case 0: case 1: case 2: - dc.DrawEllipse(NoteShapeOutlineFill, NoteStrokePen, center, 15, 15); - dc.DrawEllipse(NormalNoteShapeFill, NormalNoteShapeStrokePen, center, 11, 11); + dc.DrawEllipse(NoteShapeOutlineFill, NoteStrokePen, center, NoteRadius, NoteRadius); + dc.DrawEllipse(NormalNoteShapeFill, NormalNoteShapeStrokePen, center, NoteRadius - 4, NoteRadius - 4); break; case 3: - dc.DrawEllipse(NoteShapeOutlineFill, NoteStrokePen, center, 15, 15); - dc.DrawEllipse(HoldNoteShapeFillOuter, HoldNoteShapeStrokePen, center, 11, 11); - dc.DrawEllipse(HoldNoteShapeFillInner, null, center, 5, 5); + dc.DrawEllipse(NoteShapeOutlineFill, NoteStrokePen, center, NoteRadius, NoteRadius); + dc.DrawEllipse(HoldNoteShapeFillOuter, HoldNoteShapeStrokePen, center, NoteRadius - 4, NoteRadius - 4); + dc.DrawEllipse(HoldNoteShapeFillInner, null, center, NoteRadius - 10, NoteRadius - 10); break; } } + } - RenderCompleteHandle.Set(); + protected override void OnRender(DrawingContext dc) + { + _renderCompleteHandle.Reset(); + + base.OnRender(dc); + + if (!_isPreviewing) + { + _renderCompleteHandle.Set(); + return; + } + + DrawLines(dc); + DrawNotes(dc); + + _renderCompleteHandle.Set(); } + + #endregion } } diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs b/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs index 6286490..9d1fa5f 100644 --- a/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs +++ b/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs @@ -11,6 +11,7 @@ using DereTore.Applications.StarlightDirector.Entities; using DereTore.Applications.StarlightDirector.UI.Controls.Primitives; using System.Linq; +using DereTore.Applications.StarlightDirector.UI.Controls.Models; using DereTore.Applications.StarlightDirector.UI.Windows; using LineTuple = System.Tuple; @@ -21,47 +22,15 @@ namespace DereTore.Applications.StarlightDirector.UI.Controls /// public partial class ScorePreviewer { - /// - /// Internal representation of notes for drawing - /// - private class SimpleNote - { - public Note Note { get; set; } - public SimpleNote HoldTarget { get; set; } - public SimpleNote SyncTarget { get; set; } - public SimpleNote GroupTarget { get; set; } - public int NoteId { get; set; } - public int Timing { get; set; } - public bool Done { get; set; } - public int Duration { get; set; } - public bool IsHoldStart { get; set; } - public double LastT { get; set; } - public double X { get; set; } - public double Y { get; set; } - public int DrawType { get; set; } - public int HitPosition { get; set; } - } - - private const double NoteMarginTop = 20; - private const double NoteMarginBottom = 60; - private const double NoteXBetween = 80; - private const double NoteSize = 30; - private const double NoteRadius = NoteSize / 2; // used by frame update thread private Score _score; private volatile bool _isPreviewing; private Task _task; - private readonly List _notes = new List(); + private readonly List _notes = new List(); private double _targetFps; private int _startTime; - private double _approachTime; - // screen positions - private double _noteStartY; - private double _noteEndY; - private List _noteX; - // window-related private readonly MainWindow _window; private bool _shouldPlayMusic; @@ -75,18 +44,14 @@ public ScorePreviewer() public void BeginPreview(Score score, double targetFps, int startTime, double approachTime) { // setup parameters + _score = score; _isPreviewing = true; _targetFps = targetFps; _startTime = startTime; - _approachTime = approachTime; - - _noteX = new List(); - for (int i = 0; i < 5; ++i) - _noteX.Add(0); - ComputePositions(); // prepare notes + foreach (var note in _score.Notes) { // can I draw it? @@ -101,22 +66,18 @@ public void BeginPreview(Score score, double targetFps, int startTime, double ap if (pos == 0) continue; - var snote = new SimpleNote + var snote = new DrawingNote { Note = note, - NoteId = note.ID, Done = false, Duration = 0, IsHoldStart = note.IsHoldStart, Timing = (int) (note.HitTiming*1000), LastT = 0, - X = _noteX[pos - 1], - Y = _noteStartY, - HitPosition = pos - 1 + HitPosition = pos - 1, + DrawType = note.Type == NoteType.Hold ? 3 : (int) note.FlickType }; - snote.DrawType = note.Type == NoteType.Hold ? 3 : (int) note.FlickType; - if (note.IsHoldStart) { snote.Duration = (int) (note.HoldTarget.HitTiming*1000) - (int) (note.HitTiming*1000); @@ -128,26 +89,45 @@ public void BeginPreview(Score score, double targetFps, int startTime, double ap _notes.Sort((a, b) => a.Timing - b.Timing); // prepare note relationships + foreach (var snote in _notes) { if (snote.IsHoldStart) { - snote.HoldTarget = _notes.FirstOrDefault(note => note.NoteId == snote.Note.HoldTargetID); + snote.HoldTarget = _notes.FirstOrDefault(note => note.Note.ID == snote.Note.HoldTargetID); } if (snote.Note.HasNextSync) { - snote.SyncTarget = _notes.FirstOrDefault(note => note.NoteId == snote.Note.NextSyncTarget.ID); + snote.SyncTarget = _notes.FirstOrDefault(note => note.Note.ID == snote.Note.NextSyncTarget.ID); } if (snote.Note.HasNextFlick) { - snote.GroupTarget = _notes.FirstOrDefault(note => note.NoteId == snote.Note.NextFlickNoteID); + snote.GroupTarget = _notes.FirstOrDefault(note => note.Note.ID == snote.Note.NextFlickNoteID); } } // music - _shouldPlayMusic = ShouldPlayMusic(); + + _shouldPlayMusic = _window != null && _window.MusicLoaded; + + // fix start time + + _startTime -= (int)approachTime; + if (_startTime < 0) + _startTime = 0; + + // prepare canvas + + MainCanvas.Initialize(_notes, approachTime); + + // go + + if (_shouldPlayMusic) + { + StartMusic(_startTime); + } _task = new Task(DrawPreviewFrame); _task.Start(); @@ -163,28 +143,9 @@ public void EndPreview() _isPreviewing = false; } - /// - /// Compute where the notes should be according to current window size - /// - private void ComputePositions() - { - _noteStartY = NoteMarginTop; - _noteEndY = MainCanvas.ActualHeight - NoteMarginBottom; - _noteX[0] = (MainCanvas.ActualWidth - 4*NoteXBetween - 5*NoteSize)/2; - for (int i = 1; i < 5; ++i) - { - _noteX[i] = _noteX[i - 1] + NoteXBetween + NoteSize; - } - } - // These methods invokes the main thread and perform the tasks #region Multithreading Invoke - private bool ShouldPlayMusic() - { - return (bool)Dispatcher.Invoke(new Func(() => _window != null && _window.MusicLoaded)); - } - private void StartMusic(double milliseconds) { Dispatcher.Invoke(new Action(() => _window.PlayMusic(milliseconds))); @@ -197,59 +158,23 @@ private void StopMusic() #endregion - private void ComputeLine(SimpleNote note) - { - // ReSharper disable once CompareOfFloatsByEqualityOperator - if (note.LastT == 0 || note.Done) - return; - - // Hold line - if (note.IsHoldStart) - { - MainCanvas.Lines.Add(new LineTuple(0, note.X, note.Y, note.HoldTarget.X, note.HoldTarget.Y)); - } - - // Sync line - // check LastT so that when HitNote arrives the line is gone - if (note.SyncTarget != null && note.LastT < 1) - { - MainCanvas.Lines.Add(new LineTuple(1, note.X, note.Y, note.SyncTarget.X, note.SyncTarget.Y)); - } - - // Flicker line - if (note.GroupTarget != null && note.LastT < 1) - { - MainCanvas.Lines.Add(new LineTuple(2, note.X, note.Y, note.GroupTarget.X, note.GroupTarget.Y)); - } - } - /// /// Running in a background thread, refresh the locations of notes periodically. It tries to keep the target frame rate. /// private void DrawPreviewFrame() { - // computation parameters + // frame rate var targetFrameTime = 1000 / _targetFps; MainCanvas.HitEffectFrames = (int) (_targetFps / 5); // effect lasts for 0.2 second - // fix start time - _startTime -= (int)_approachTime; - if (_startTime < 0) - _startTime = 0; - // drawing and timing var startTime = DateTime.UtcNow; - var notesHead = 0; - - if (_shouldPlayMusic) - { - StartMusic(_startTime); - } while (true) { if (!_isPreviewing) { + MainCanvas.Stop(); _notes.Clear(); return; } @@ -268,77 +193,9 @@ private void DrawPreviewFrame() songTime = (int)(frameStartTime - startTime).TotalMilliseconds + _startTime; } - // reset canvas - - MainCanvas.Notes.Clear(); - MainCanvas.Lines.Clear(); - - // compute notes - - int computeStart = notesHead; - int computeEnd = notesHead; - bool headUpdated = false; - for (int i = notesHead; i < _notes.Count; ++i) - { - var note = _notes[i]; - - /* - * diff < 0 --- not shown - * diff == 0 --- begin to show - * diff == approachTime --- arrive at end - * diff > approachTime --- ended - */ - var diff = songTime - note.Timing + _approachTime; - if (diff < 0) - break; - if (note.Done) - continue; - - computeEnd = i; - - var t = diff/ _approachTime; - if (t > 1) - t = 1; - - note.LastT = t; - - // change this line if you want game-like approaching path - note.Y = _noteStartY + t*(_noteEndY - _noteStartY); - - MainCanvas.Notes.Add(new Tuple(note.DrawType, note.X, note.Y)); - - // note arrive at bottom - if (diff > _approachTime) - { - MainCanvas.NoteHit(note.HitPosition); - - // Hit and flick notes end immediately - // Hold note heads end after its duration - if (!note.IsHoldStart || diff > _approachTime + note.Duration) - { - note.Done = true; - } - } - - // update head to be the first note that is not done - if (!note.Done && !headUpdated) - { - notesHead = i; - headUpdated = true; - } - } - - // compute lines - - for (int i = computeStart; i <= computeEnd; ++i) - { - ComputeLine(_notes[i]); - } - // wait for rendering - Dispatcher.Invoke(new Action(() => MainCanvas.InvalidateVisual())); - MainCanvas.RenderCompleteHandle.WaitOne(); + MainCanvas.RenderFrameBlocked(songTime); // wait for next frame From 6ab869b9718ba036a369c57823e5577ec95c43ce Mon Sep 17 00:00:00 2001 From: logchan Date: Fri, 18 Nov 2016 16:50:10 +0800 Subject: [PATCH 04/13] Add grid line and effects, fix draw note Fix: draw some notes that are done --- .../UI/Controls/Models/DrawingNote.cs | 1 + .../UI/Controls/PreviewCanvas.cs | 44 ++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/Models/DrawingNote.cs b/DereTore.Applications.StarlightDirector/UI/Controls/Models/DrawingNote.cs index 04203a0..601bc8e 100644 --- a/DereTore.Applications.StarlightDirector/UI/Controls/Models/DrawingNote.cs +++ b/DereTore.Applications.StarlightDirector/UI/Controls/Models/DrawingNote.cs @@ -24,5 +24,6 @@ public class DrawingNote public double Y { get; set; } public int DrawType { get; set; } public int HitPosition { get; set; } + public bool EffectShown { get; set; } } } diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs b/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs index 3ef7be6..e6c5af3 100644 --- a/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs +++ b/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs @@ -24,6 +24,8 @@ public class PreviewCanvas : Canvas private double _noteStartY; private double _noteEndY; private readonly List _noteX = new List(); + private readonly List _startPoints = new List(); + private readonly List _endPoints = new List(); // computation private List _notes; @@ -65,6 +67,12 @@ public void Initialize(List notes, double approachTime) _noteX.Add(_noteX.Last() + NoteXBetween + NoteSize); } + for (int i = 0; i < 5; ++i) + { + _startPoints.Add(new Point(_noteX[i], _noteStartY)); + _endPoints.Add(new Point(_noteX[i], _noteEndY)); + } + // initialize note positions foreach (var note in _notes) @@ -131,6 +139,8 @@ public void NoteHit(int position) private static readonly Pen FlickNoteShapeStrokePen = new Pen(FlickNoteShapeStroke, 1); + private static readonly Pen GridPen = new Pen(Brushes.DarkGray, 1); + private static readonly Brush RelationBrush = Application.Current.FindResource(App.ResourceKeys.RelationBorderBrush); private static readonly Pen[] LinePens = @@ -180,7 +190,11 @@ private void ComputeFrame(int songTime) // note arrive at bottom if (diff > _approachTime) { - NoteHit(note.HitPosition); + if (!note.EffectShown) + { + NoteHit(note.HitPosition); + note.EffectShown = true; + } // Hit and flick notes end immediately // Hold note heads end after its duration @@ -203,6 +217,14 @@ private void ComputeFrame(int songTime) #region Rendering + private void DrawGrid(DrawingContext dc) + { + for (int i = 0; i < 5; ++i) + { + dc.DrawLine(GridPen, _startPoints[i], _endPoints[i]); + } + } + private void DrawLines(DrawingContext dc) { for (int i = _notesHead; i <= _notesTail; ++i) @@ -239,6 +261,9 @@ private void DrawNotes(DrawingContext dc) for (int i = _notesHead; i <= _notesTail; ++i) { var note = _notes[i]; + if (note.Done) + continue; + var center = new Point(note.X, note.Y); switch (note.DrawType) @@ -258,6 +283,21 @@ private void DrawNotes(DrawingContext dc) } } + private void DrawEffect(DrawingContext dc) + { + for (int i = 0; i < 5; ++i) + { + if (_hitEffectCountdown[i] == 0) + continue; + + var t = _hitEffectCountdown[i]/(double) HitEffectFrames; + dc.DrawEllipse(new SolidColorBrush(Color.FromArgb((byte)(t*200), 0xFF, 0xFF, 0)), null, _endPoints[i], NoteSize, + NoteSize); + + --_hitEffectCountdown[i]; + } + } + protected override void OnRender(DrawingContext dc) { _renderCompleteHandle.Reset(); @@ -270,7 +310,9 @@ protected override void OnRender(DrawingContext dc) return; } + DrawGrid(dc); DrawLines(dc); + DrawEffect(dc); DrawNotes(dc); _renderCompleteHandle.Set(); From cba25b04f6318a3b24926251b3cbaec9d6ef04ea Mon Sep 17 00:00:00 2001 From: logchan Date: Fri, 18 Nov 2016 18:07:22 +0800 Subject: [PATCH 05/13] Draw flick notes correctly --- .../UI/Controls/PreviewCanvas.cs | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs b/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs index e6c5af3..30d4bc6 100644 --- a/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs +++ b/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs @@ -150,6 +150,16 @@ public void NoteHit(int position) new Pen(RelationBrush, 8) // group line }; + private static readonly Geometry LeftNoteOuterGeometry = + Geometry.Parse("M -6,15 A 24,24 0 0 1 15,0 A 15,15 0 0 1 15,30 A 24,24 0 0 1 -6,15 Z"); + private static readonly Geometry LeftNoteInnerGeometry = + Geometry.Parse("M -6,15 C 6,-2 20,-2 15,15 C 21,32 6,32 -6,15 Z"); + + private static readonly Geometry RightNoteOuterGeometry = + Geometry.Parse("M 36,15 A 24,24 0 0 0 15,0 A 15,15 0 0 0 15,30 A 24,24 0 0 0 36,15 Z"); + private static readonly Geometry RightNoteInnerGeometry = + Geometry.Parse("M 36,15 C 24,-2 16,-2 15,15 C 16,32 24,32 36,15 Z"); + #endregion #region Computation and Positions @@ -269,11 +279,31 @@ private void DrawNotes(DrawingContext dc) switch (note.DrawType) { case 0: - case 1: - case 2: dc.DrawEllipse(NoteShapeOutlineFill, NoteStrokePen, center, NoteRadius, NoteRadius); dc.DrawEllipse(NormalNoteShapeFill, NormalNoteShapeStrokePen, center, NoteRadius - 4, NoteRadius - 4); break; + case 1: + dc.PushTransform(new TranslateTransform(note.X - 15, note.Y - 15)); + dc.DrawGeometry(NoteShapeOutlineFill, NoteStrokePen, LeftNoteOuterGeometry); + dc.PushTransform(new ScaleTransform(0.722, 0.722, 15, 15)); + dc.DrawGeometry(FlickNoteShapeFillOuter, FlickNoteShapeStrokePen, LeftNoteOuterGeometry); + dc.Pop(); + dc.PushTransform(new ScaleTransform(0.5714, 0.5714, 15, 15)); + dc.DrawGeometry(FlickNoteShapeFillInner, null, LeftNoteInnerGeometry); + dc.Pop(); + dc.Pop(); + break; + case 2: + dc.PushTransform(new TranslateTransform(note.X - 15, note.Y - 15)); + dc.DrawGeometry(NoteShapeOutlineFill, NoteStrokePen, RightNoteOuterGeometry); + dc.PushTransform(new ScaleTransform(0.722, 0.722, 15, 15)); + dc.DrawGeometry(FlickNoteShapeFillOuter, FlickNoteShapeStrokePen, RightNoteOuterGeometry); + dc.Pop(); + dc.PushTransform(new ScaleTransform(0.5714, 0.5714, 15, 15)); + dc.DrawGeometry(FlickNoteShapeFillInner, null, RightNoteInnerGeometry); + dc.Pop(); + dc.Pop(); + break; case 3: dc.DrawEllipse(NoteShapeOutlineFill, NoteStrokePen, center, NoteRadius, NoteRadius); dc.DrawEllipse(HoldNoteShapeFillOuter, HoldNoteShapeStrokePen, center, NoteRadius - 4, NoteRadius - 4); From 094f13eb3c869d303fe47a286520fb37780c9d15 Mon Sep 17 00:00:00 2001 From: logchan Date: Fri, 18 Nov 2016 18:20:47 +0800 Subject: [PATCH 06/13] Make things static readonly --- .../UI/Controls/PreviewCanvas.cs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs b/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs index 30d4bc6..2246bfa 100644 --- a/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs +++ b/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs @@ -160,6 +160,11 @@ public void NoteHit(int position) private static readonly Geometry RightNoteInnerGeometry = Geometry.Parse("M 36,15 C 24,-2 16,-2 15,15 C 16,32 24,32 36,15 Z"); + private static readonly ScaleTransform FlickNoteOuterScale = new ScaleTransform(0.722, 0.722, 15, 15); + private static readonly ScaleTransform FlickNoteInnerScale = new ScaleTransform(0.5714, 0.5714, 15, 15); + + private static readonly SolidColorBrush HitEffectBrush = Brushes.Gold; + #endregion #region Computation and Positions @@ -252,7 +257,7 @@ private void DrawLines(DrawingContext dc) } // Sync line - // check LastT so that when HitNote arrives the line is gone + // check LastT so that when HoldNote arrives the line is gone if (note.SyncTarget != null && note.LastT < 1) { dc.DrawLine(LinePens[1], new Point(note.X, note.Y), new Point(note.SyncTarget.X, note.SyncTarget.Y)); @@ -276,6 +281,7 @@ private void DrawNotes(DrawingContext dc) var center = new Point(note.X, note.Y); + switch (note.DrawType) { case 0: @@ -285,10 +291,10 @@ private void DrawNotes(DrawingContext dc) case 1: dc.PushTransform(new TranslateTransform(note.X - 15, note.Y - 15)); dc.DrawGeometry(NoteShapeOutlineFill, NoteStrokePen, LeftNoteOuterGeometry); - dc.PushTransform(new ScaleTransform(0.722, 0.722, 15, 15)); + dc.PushTransform(FlickNoteOuterScale); dc.DrawGeometry(FlickNoteShapeFillOuter, FlickNoteShapeStrokePen, LeftNoteOuterGeometry); dc.Pop(); - dc.PushTransform(new ScaleTransform(0.5714, 0.5714, 15, 15)); + dc.PushTransform(FlickNoteInnerScale); dc.DrawGeometry(FlickNoteShapeFillInner, null, LeftNoteInnerGeometry); dc.Pop(); dc.Pop(); @@ -296,10 +302,10 @@ private void DrawNotes(DrawingContext dc) case 2: dc.PushTransform(new TranslateTransform(note.X - 15, note.Y - 15)); dc.DrawGeometry(NoteShapeOutlineFill, NoteStrokePen, RightNoteOuterGeometry); - dc.PushTransform(new ScaleTransform(0.722, 0.722, 15, 15)); + dc.PushTransform(FlickNoteOuterScale); dc.DrawGeometry(FlickNoteShapeFillOuter, FlickNoteShapeStrokePen, RightNoteOuterGeometry); dc.Pop(); - dc.PushTransform(new ScaleTransform(0.5714, 0.5714, 15, 15)); + dc.PushTransform(FlickNoteInnerScale); dc.DrawGeometry(FlickNoteShapeFillInner, null, RightNoteInnerGeometry); dc.Pop(); dc.Pop(); @@ -321,8 +327,11 @@ private void DrawEffect(DrawingContext dc) continue; var t = _hitEffectCountdown[i]/(double) HitEffectFrames; - dc.DrawEllipse(new SolidColorBrush(Color.FromArgb((byte)(t*200), 0xFF, 0xFF, 0)), null, _endPoints[i], NoteSize, + + dc.PushOpacity(t); + dc.DrawEllipse(HitEffectBrush, null, _endPoints[i], NoteSize, NoteSize); + dc.Pop(); --_hitEffectCountdown[i]; } From a484e569fb520681ec462e6d2f485b3a6e10ab59 Mon Sep 17 00:00:00 2001 From: logchan Date: Fri, 18 Nov 2016 21:27:23 +0800 Subject: [PATCH 07/13] Fix bugs Fix missing lines Fix incorrect hold ending drawings --- .../UI/Controls/PreviewCanvas.cs | 2 +- .../UI/Controls/ScorePreviewer.xaml.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs b/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs index 2246bfa..0689924 100644 --- a/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs +++ b/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs @@ -248,7 +248,7 @@ private void DrawLines(DrawingContext dc) // ReSharper disable once CompareOfFloatsByEqualityOperator if (note.LastT == 0 || note.Done) - return; + continue; // Hold line if (note.IsHoldStart) diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs b/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs index 9d1fa5f..d05edcc 100644 --- a/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs +++ b/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs @@ -75,7 +75,7 @@ public void BeginPreview(Score score, double targetFps, int startTime, double ap Timing = (int) (note.HitTiming*1000), LastT = 0, HitPosition = pos - 1, - DrawType = note.Type == NoteType.Hold ? 3 : (int) note.FlickType + DrawType = (note.IsTap && !note.IsHoldEnd) || note.IsFlick ? (int)note.FlickType : 3 }; if (note.IsHoldStart) From 25dbeda93009b7f94749e94196e6a529896e096c Mon Sep 17 00:00:00 2001 From: logchan Date: Fri, 18 Nov 2016 22:13:05 +0800 Subject: [PATCH 08/13] Enhancements Add more FPS options Rewrite MusicTime() Handle song end --- .../UI/Controls/PreviewCanvas.cs | 46 +++++++++++++------ .../UI/Controls/ScorePreviewer.xaml.cs | 41 ++++++++++------- .../UI/Windows/MainWindow.Commands.Music.cs | 4 +- .../UI/Windows/MainWindow.EventHandlers.cs | 7 +++ .../UI/Windows/MainWindow.xaml | 3 ++ 5 files changed, 69 insertions(+), 32 deletions(-) diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs b/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs index 0689924..40f0ec6 100644 --- a/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs +++ b/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs @@ -35,17 +35,20 @@ public class PreviewCanvas : Canvas // rendering private volatile bool _isPreviewing; - private readonly List _hitEffectCountdown; + private readonly List _hitEffectStartTime; + private readonly List _hitEffectT; private readonly EventWaitHandle _renderCompleteHandle = new EventWaitHandle(false, EventResetMode.AutoReset); - public int HitEffectFrames { get; set; } + public double HitEffectMilliseconds { get; set; } public PreviewCanvas() { - _hitEffectCountdown = new List(); + _hitEffectStartTime = new List(); + _hitEffectT = new List(); for (int i = 0; i < 5; ++i) { - _hitEffectCountdown.Add(0); + _hitEffectStartTime.Add(0); + _hitEffectT.Add(0); } } @@ -97,9 +100,9 @@ public void Stop() _isPreviewing = false; } - public void NoteHit(int position) + private void NoteHit(int position, int songTime) { - _hitEffectCountdown[position] = HitEffectFrames; + _hitEffectStartTime[position] = songTime; } #region Brushes and Pens @@ -207,7 +210,7 @@ private void ComputeFrame(int songTime) { if (!note.EffectShown) { - NoteHit(note.HitPosition); + NoteHit(note.HitPosition, songTime); note.EffectShown = true; } @@ -226,6 +229,23 @@ private void ComputeFrame(int songTime) headUpdated = true; } } + + // Update hit effects + for (int i = 0; i < 5; ++i) + { + if (_hitEffectStartTime[i] == 0) + continue; + + var diff = songTime - _hitEffectStartTime[i]; + if (diff <= HitEffectMilliseconds) + { + _hitEffectT[i] = 1 - diff/HitEffectMilliseconds; + } + else + { + _hitEffectT[i] = 0; + } + } } #endregion @@ -323,17 +343,15 @@ private void DrawEffect(DrawingContext dc) { for (int i = 0; i < 5; ++i) { - if (_hitEffectCountdown[i] == 0) - continue; + var t = _hitEffectT[i]; - var t = _hitEffectCountdown[i]/(double) HitEffectFrames; + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (t == 0) + continue; dc.PushOpacity(t); - dc.DrawEllipse(HitEffectBrush, null, _endPoints[i], NoteSize, - NoteSize); + dc.DrawEllipse(HitEffectBrush, null, _endPoints[i], NoteSize, NoteSize); dc.Pop(); - - --_hitEffectCountdown[i]; } } diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs b/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs index d05edcc..c5c8ecc 100644 --- a/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs +++ b/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs @@ -4,16 +4,10 @@ using System.Threading; using System.Threading.Tasks; using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; -using System.Windows.Shapes; -using DereTore.Applications.StarlightDirector.Extensions; using DereTore.Applications.StarlightDirector.Entities; -using DereTore.Applications.StarlightDirector.UI.Controls.Primitives; using System.Linq; using DereTore.Applications.StarlightDirector.UI.Controls.Models; using DereTore.Applications.StarlightDirector.UI.Windows; -using LineTuple = System.Tuple; namespace DereTore.Applications.StarlightDirector.UI.Controls { @@ -164,8 +158,13 @@ private void StopMusic() private void DrawPreviewFrame() { // frame rate - var targetFrameTime = 1000 / _targetFps; - MainCanvas.HitEffectFrames = (int) (_targetFps / 5); // effect lasts for 0.2 second + double targetFrameTime = 0; + if (_targetFps < Double.MaxValue) + { + targetFrameTime = 1000/_targetFps; + } + + MainCanvas.HitEffectMilliseconds = 200; // drawing and timing var startTime = DateTime.UtcNow; @@ -186,7 +185,14 @@ private void DrawPreviewFrame() int songTime; if (_shouldPlayMusic) { - songTime = (int)_window.MusicTime().TotalMilliseconds; + var time = _window.MusicTime(); + if (time == Double.MaxValue) + { + EndPreview(); + continue; + } + + songTime = (int)time; } else { @@ -199,14 +205,17 @@ private void DrawPreviewFrame() // wait for next frame - var frameEllapsedTime = (DateTime.UtcNow - frameStartTime).TotalMilliseconds; - if (frameEllapsedTime < targetFrameTime) - { - Thread.Sleep((int) (targetFrameTime - frameEllapsedTime)); - } - else + if (targetFrameTime > 0) { - Debug.WriteLine($"[Warning] Frame ellapsed time {frameEllapsedTime:N2} exceeds target."); + var frameEllapsedTime = (DateTime.UtcNow - frameStartTime).TotalMilliseconds; + if (frameEllapsedTime < targetFrameTime) + { + Thread.Sleep((int)(targetFrameTime - frameEllapsedTime)); + } + else + { + Debug.WriteLine($"[Warning] Frame ellapsed time {frameEllapsedTime:N2} exceeds target."); + } } } } diff --git a/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.Commands.Music.cs b/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.Commands.Music.cs index 9840527..1423088 100644 --- a/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.Commands.Music.cs +++ b/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.Commands.Music.cs @@ -108,9 +108,9 @@ internal void StopMusic() CmdMusicStop.RaiseCanExecuteChanged(); } - internal TimeSpan MusicTime() + internal double MusicTime() { - return _waveReader.CurrentTime; + return _waveReader?.CurrentTime.TotalMilliseconds ?? Double.MaxValue; } private AudioOut _selectedWaveOut; diff --git a/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.EventHandlers.cs b/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.EventHandlers.cs index 464c9bf..f49ce11 100644 --- a/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.EventHandlers.cs +++ b/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.EventHandlers.cs @@ -110,6 +110,13 @@ private void PreviewFpsComboBoxItem_Selected(object sender, System.Windows.Route { var item = e.OriginalSource as ComboBoxItem; double fps; + var s = item?.Content?.ToString(); + if (s == "Unlimited") + { + PreviewFps = Double.MaxValue; + return; + } + if (Double.TryParse(item?.Content?.ToString(), out fps)) { PreviewFps = fps; diff --git a/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.xaml b/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.xaml index 161f4bc..5caf76b 100644 --- a/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.xaml +++ b/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.xaml @@ -181,6 +181,9 @@ + + + From d87768012ace242ed14e3e65a554c964f9ea5116 Mon Sep 17 00:00:00 2001 From: logchan Date: Fri, 18 Nov 2016 22:35:15 +0800 Subject: [PATCH 09/13] Fix hit effect timing Reset at preview start --- .../UI/Controls/PreviewCanvas.cs | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs b/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs index 40f0ec6..94dde75 100644 --- a/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs +++ b/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs @@ -35,21 +35,15 @@ public class PreviewCanvas : Canvas // rendering private volatile bool _isPreviewing; - private readonly List _hitEffectStartTime; - private readonly List _hitEffectT; + private List _hitEffectStartTime; + private List _hitEffectT; private readonly EventWaitHandle _renderCompleteHandle = new EventWaitHandle(false, EventResetMode.AutoReset); public double HitEffectMilliseconds { get; set; } public PreviewCanvas() { - _hitEffectStartTime = new List(); - _hitEffectT = new List(); - for (int i = 0; i < 5; ++i) - { - _hitEffectStartTime.Add(0); - _hitEffectT.Add(0); - } + } public void Initialize(List notes, double approachTime) @@ -84,6 +78,16 @@ public void Initialize(List notes, double approachTime) note.Y = _noteStartY; } + // hit effect timing + + _hitEffectStartTime = new List(); + _hitEffectT = new List(); + for (int i = 0; i < 5; ++i) + { + _hitEffectStartTime.Add(0); + _hitEffectT.Add(0); + } + _isPreviewing = true; } From 04acf113581a54f075fb53b608fbc14dcec414ea Mon Sep 17 00:00:00 2001 From: logchan Date: Fri, 18 Nov 2016 22:35:45 +0800 Subject: [PATCH 10/13] Fix song timing Need to use frame time when MusicTime() is not updated --- .../UI/Controls/ScorePreviewer.xaml.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs b/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs index c5c8ecc..bb62a01 100644 --- a/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs +++ b/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs @@ -25,6 +25,10 @@ public partial class ScorePreviewer private double _targetFps; private int _startTime; + // music time fixing + private int _lastSongTime = 0; + private DateTime _lastFrameEndtime; + // window-related private readonly MainWindow _window; private bool _shouldPlayMusic; @@ -193,6 +197,11 @@ private void DrawPreviewFrame() } songTime = (int)time; + if (songTime > 0 && songTime == _lastSongTime) + { + songTime += (int)(frameStartTime - _lastFrameEndtime).TotalMilliseconds; + } + _lastSongTime = songTime; } else { @@ -207,7 +216,8 @@ private void DrawPreviewFrame() if (targetFrameTime > 0) { - var frameEllapsedTime = (DateTime.UtcNow - frameStartTime).TotalMilliseconds; + _lastFrameEndtime = DateTime.UtcNow; + var frameEllapsedTime = (_lastFrameEndtime - frameStartTime).TotalMilliseconds; if (frameEllapsedTime < targetFrameTime) { Thread.Sleep((int)(targetFrameTime - frameEllapsedTime)); From cec78f4d8642ee15aa3441c171f2cfbff5e679c9 Mon Sep 17 00:00:00 2001 From: logchan Date: Fri, 18 Nov 2016 22:37:45 +0800 Subject: [PATCH 11/13] Fix last commit :new_moon_with_face: --- .../UI/Controls/ScorePreviewer.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs b/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs index bb62a01..cc13d1e 100644 --- a/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs +++ b/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs @@ -214,9 +214,9 @@ private void DrawPreviewFrame() // wait for next frame + _lastFrameEndtime = DateTime.UtcNow; if (targetFrameTime > 0) { - _lastFrameEndtime = DateTime.UtcNow; var frameEllapsedTime = (_lastFrameEndtime - frameStartTime).TotalMilliseconds; if (frameEllapsedTime < targetFrameTime) { From ed7ccba3f4a582307cdfcb68dc050f8cbdd0ad73 Mon Sep 17 00:00:00 2001 From: logchan Date: Fri, 18 Nov 2016 22:44:54 +0800 Subject: [PATCH 12/13] Change default FPS to 120 --- .../UI/Windows/MainWindow.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.xaml b/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.xaml index 5caf76b..6260111 100644 --- a/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.xaml +++ b/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.xaml @@ -178,7 +178,7 @@ - + From 7c013ffda8b80feafdb118d94c093e67a8976709 Mon Sep 17 00:00:00 2001 From: logchan Date: Fri, 18 Nov 2016 23:27:55 +0800 Subject: [PATCH 13/13] Fix time compute logic --- .../UI/Controls/ScorePreviewer.xaml.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs b/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs index cc13d1e..e15e7a9 100644 --- a/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs +++ b/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs @@ -26,7 +26,8 @@ public partial class ScorePreviewer private int _startTime; // music time fixing - private int _lastSongTime = 0; + private int _lastMusicTime; + private int _lastComputedSongTime; private DateTime _lastFrameEndtime; // window-related @@ -197,11 +198,18 @@ private void DrawPreviewFrame() } songTime = (int)time; - if (songTime > 0 && songTime == _lastSongTime) + if (songTime > 0 && songTime == _lastMusicTime) { - songTime += (int)(frameStartTime - _lastFrameEndtime).TotalMilliseconds; + // music time not updated, add frame time + _lastComputedSongTime += (int) (frameStartTime - _lastFrameEndtime).TotalMilliseconds; + songTime = _lastComputedSongTime; + } + else + { + // music time updated + _lastComputedSongTime = songTime; + _lastMusicTime = songTime; } - _lastSongTime = songTime; } else {