diff --git a/DereTore.Applications.StarlightDirector/DereTore.Applications.StarlightDirector.csproj b/DereTore.Applications.StarlightDirector/DereTore.Applications.StarlightDirector.csproj
index 4d6ecbf..01308c9 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
@@ -203,6 +199,8 @@
+
+
LineLayer.xaml
@@ -235,9 +233,6 @@
-
- SimpleScoreNote.xaml
-
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..601bc8e
--- /dev/null
+++ b/DereTore.Applications.StarlightDirector/UI/Controls/Models/DrawingNote.cs
@@ -0,0 +1,29 @@
+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; }
+ public bool EffectShown { get; set; }
+ }
+}
diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs b/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs
new file mode 100644
index 0000000..94dde75
--- /dev/null
+++ b/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs
@@ -0,0 +1,384 @@
+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;
+using DereTore.Applications.StarlightDirector.UI.Controls.Models;
+
+namespace DereTore.Applications.StarlightDirector.UI.Controls
+{
+ public class PreviewCanvas : Canvas
+ {
+ // 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();
+ private readonly List _startPoints = new List();
+ private readonly List _endPoints = new List();
+
+ // computation
+ private List _notes;
+ private int _notesHead;
+ private int _notesTail;
+ private double _approachTime;
+
+ // rendering
+ private volatile bool _isPreviewing;
+ private List _hitEffectStartTime;
+ private List _hitEffectT;
+ private readonly EventWaitHandle _renderCompleteHandle = new EventWaitHandle(false, EventResetMode.AutoReset);
+
+ public double HitEffectMilliseconds { get; set; }
+
+ 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);
+ }
+
+ 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)
+ {
+ note.X = _noteX[note.HitPosition];
+ 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;
+ }
+
+ public void RenderFrameBlocked(int songTime)
+ {
+ ComputeFrame(songTime);
+
+ Dispatcher.Invoke(new Action(InvalidateVisual));
+ _renderCompleteHandle.WaitOne();
+ }
+
+ public void Stop()
+ {
+ _isPreviewing = false;
+ }
+
+ private void NoteHit(int position, int songTime)
+ {
+ _hitEffectStartTime[position] = songTime;
+ }
+
+ #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 Pen GridPen = new Pen(Brushes.DarkGray, 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
+ };
+
+ 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");
+
+ 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
+
+ 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)
+ {
+ if (!note.EffectShown)
+ {
+ NoteHit(note.HitPosition, songTime);
+ note.EffectShown = true;
+ }
+
+ // 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;
+ }
+ }
+
+ // 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
+
+ #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)
+ {
+ var note = _notes[i];
+
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ if (note.LastT == 0 || note.Done)
+ continue;
+
+ // Hold line
+ if (note.IsHoldStart)
+ {
+ dc.DrawLine(LinePens[0], new Point(note.X, note.Y), new Point(note.HoldTarget.X, note.HoldTarget.Y));
+ }
+
+ // Sync line
+ // 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));
+ }
+
+ // 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));
+ }
+ }
+ }
+
+ 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)
+ {
+ case 0:
+ 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(FlickNoteOuterScale);
+ dc.DrawGeometry(FlickNoteShapeFillOuter, FlickNoteShapeStrokePen, LeftNoteOuterGeometry);
+ dc.Pop();
+ dc.PushTransform(FlickNoteInnerScale);
+ 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(FlickNoteOuterScale);
+ dc.DrawGeometry(FlickNoteShapeFillOuter, FlickNoteShapeStrokePen, RightNoteOuterGeometry);
+ dc.Pop();
+ dc.PushTransform(FlickNoteInnerScale);
+ 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);
+ dc.DrawEllipse(HoldNoteShapeFillInner, null, center, NoteRadius - 10, NoteRadius - 10);
+ break;
+ }
+ }
+ }
+
+ private void DrawEffect(DrawingContext dc)
+ {
+ for (int i = 0; i < 5; ++i)
+ {
+ var t = _hitEffectT[i];
+
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ if (t == 0)
+ continue;
+
+ dc.PushOpacity(t);
+ dc.DrawEllipse(HitEffectBrush, null, _endPoints[i], NoteSize, NoteSize);
+ dc.Pop();
+ }
+ }
+
+ protected override void OnRender(DrawingContext dc)
+ {
+ _renderCompleteHandle.Reset();
+
+ base.OnRender(dc);
+
+ if (!_isPreviewing)
+ {
+ _renderCompleteHandle.Set();
+ return;
+ }
+
+ DrawGrid(dc);
+ DrawLines(dc);
+ DrawEffect(dc);
+ DrawNotes(dc);
+
+ _renderCompleteHandle.Set();
+ }
+
+ #endregion
+ }
+}
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); }
- }
- }
-}
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..e15e7a9 100644
--- a/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs
+++ b/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs
@@ -4,15 +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
{
@@ -21,69 +16,24 @@ 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 SimpleScoreNote ScoreNote { get; set; }
- public Line HoldLine { get; set; }
- public Line GroupLine { get; set; }
- public Line SyncLine { 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;
- private const double HoldLineThickness = 16;
- private const double SyncLineThickness = 8;
- private const double GridLineThickness = 1;
// 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;
- // 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;
+ // music time fixing
+ private int _lastMusicTime;
+ private int _lastComputedSongTime;
+ private DateTime _lastFrameEndtime;
// window-related
private readonly MainWindow _window;
private bool _shouldPlayMusic;
- // note hit effect!
- private int _noteHitCounter;
- private int _noteHitMax;
- private Line _effectedLine;
-
public ScorePreviewer()
{
InitializeComponent();
@@ -93,18 +43,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?
@@ -119,17 +65,16 @@ 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,
+ DrawType = (note.IsTap && !note.IsHoldEnd) || note.IsFlick ? (int)note.FlickType : 3
};
if (note.IsHoldStart)
@@ -143,99 +88,63 @@ 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);
}
}
- // draw some grid lines
- DrawGrid();
-
// music
- _shouldPlayMusic = ShouldPlayMusic();
- _task = new Task(DrawPreviewFrame);
- _task.Start();
- }
+ _shouldPlayMusic = _window != null && _window.MusicLoaded;
- public void EndPreview()
- {
- if (_shouldPlayMusic)
- {
- StopMusic();
- }
+ // fix start time
- _isPreviewing = false;
- }
+ _startTime -= (int)approachTime;
+ if (_startTime < 0)
+ _startTime = 0;
- ///
- /// Compute where the notes should be according to current window size
- ///
- private void ComputePositions()
- {
- _noteStartY = NoteMarginTop;
- _noteEndY = PreviewCanvas.ActualHeight - NoteMarginBottom;
- _noteX[0] = (PreviewCanvas.ActualWidth - 4*NoteXBetween - 5*NoteSize)/2;
- for (int i = 1; i < 5; ++i)
- {
- _noteX[i] = _noteX[i - 1] + NoteXBetween + NoteSize;
- }
+ // prepare canvas
- _lineOffset = NoteRadius;
- }
+ MainCanvas.Initialize(_notes, approachTime);
- private void DrawGrid()
- {
- var lines = new List();
+ // go
- foreach (var x in _noteX)
+ if (_shouldPlayMusic)
{
- lines.Add(new LineTuple(x, _noteStartY, x, _noteEndY));
+ StartMusic(_startTime);
}
- 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)
+ _task = new Task(DrawPreviewFrame);
+ _task.Start();
+ }
+
+ public void EndPreview()
+ {
+ if (_shouldPlayMusic)
{
- // 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);
+ StopMusic();
}
- _effectedLine.Stroke = Brushes.Gold;
- _effectedLine.Opacity = 0;
+ _isPreviewing = false;
}
// 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)));
@@ -246,285 +155,85 @@ 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);
- }));
- }
-
- private void ReleaseLine(Line line)
- {
- if (line == null)
- 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)
- {
- line = new Line
- {
- Stroke = _relationBrush,
- StrokeThickness = thickness
- };
- LineCanvas.Children.Add(line);
- }
- else
- {
- line = _linePool.Dequeue();
- line.Visibility = Visibility.Visible;
- line.StrokeThickness = thickness;
- }
-
- return line;
- }
-
- ///
- /// Draw lines. MUST BE CALLED ON MAIN THREAD
- ///
- private void DrawLines()
- {
- foreach (var note in _notes)
- {
- // 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;
- }
- }
- }
-
///
/// Running in a background thread, refresh the locations of notes periodically. It tries to keep the target frame rate.
///
private void DrawPreviewFrame()
{
- // computation parameters
- var targetFrameTime = 1000 / _targetFps;
- _noteHitMax = (int) (_targetFps / 5); // effect lasts for 0.2 second
+ // frame rate
+ double targetFrameTime = 0;
+ if (_targetFps < Double.MaxValue)
+ {
+ targetFrameTime = 1000/_targetFps;
+ }
- // fix start time
- _startTime -= (int)_approachTime;
- if (_startTime < 0)
- _startTime = 0;
+ MainCanvas.HitEffectMilliseconds = 200;
// drawing and timing
var startTime = DateTime.UtcNow;
- var notesHead = 0;
-
- if (_shouldPlayMusic)
- {
- StartMusic(_startTime);
- }
while (true)
{
if (!_isPreviewing)
{
- ClearCanvases();
-
- _linePool.Clear();
+ MainCanvas.Stop();
_notes.Clear();
- _scoreNotePool.Clear();
return;
}
var frameStartTime = DateTime.UtcNow;
+ // compute time
+
int songTime;
if (_shouldPlayMusic)
{
- songTime = (int)_window.MusicTime().TotalMilliseconds;
+ var time = _window.MusicTime();
+ if (time == Double.MaxValue)
+ {
+ EndPreview();
+ continue;
+ }
+
+ songTime = (int)time;
+ if (songTime > 0 && songTime == _lastMusicTime)
+ {
+ // music time not updated, add frame time
+ _lastComputedSongTime += (int) (frameStartTime - _lastFrameEndtime).TotalMilliseconds;
+ songTime = _lastComputedSongTime;
+ }
+ else
+ {
+ // music time updated
+ _lastComputedSongTime = songTime;
+ _lastMusicTime = songTime;
+ }
}
else
{
songTime = (int)(frameStartTime - startTime).TotalMilliseconds + _startTime;
}
- 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;
-
- if (note.ScoreNote == null)
- {
- note.ScoreNote = CreateScoreNote(note);
- }
-
- var t = diff/ _approachTime;
- if (t > 1)
- t = 1;
+ // wait for rendering
- note.LastT = t;
+ MainCanvas.RenderFrameBlocked(songTime);
- // change this line if you want game-like approaching path
- note.Y = _noteStartY + t*(_noteEndY - _noteStartY);
- SetPositionInCanvas(note.ScoreNote, note.X, note.Y);
+ // wait for next frame
- // note arrive at bottom
- if (diff > _approachTime)
+ _lastFrameEndtime = DateTime.UtcNow;
+ if (targetFrameTime > 0)
+ {
+ var frameEllapsedTime = (_lastFrameEndtime - frameStartTime).TotalMilliseconds;
+ if (frameEllapsedTime < targetFrameTime)
{
- ReleaseLine(note.SyncLine);
- ReleaseLine(note.GroupLine);
- note.SyncLine = null;
- note.GroupLine = null;
-
- // 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;
- }
+ Thread.Sleep((int)(targetFrameTime - frameEllapsedTime));
}
- }
-
- // Draw sync, group, hold lines
- Dispatcher.Invoke(new Action(DrawLines));
-
- // Do note hit effect
- if (_noteHitCounter >= 0)
- {
- Dispatcher.Invoke(new Action(() =>
+ else
{
- double et = _noteHitCounter/(double) _noteHitMax; // from 1 to 0
- _effectedLine.Opacity = et;
- }));
- --_noteHitCounter;
- }
-
- 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.");
+ 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..6260111 100644
--- a/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.xaml
+++ b/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.xaml
@@ -178,9 +178,12 @@
-
+
+
+
+