diff --git a/Readme.md b/Readme.md index 399a39d..b0932f0 100644 --- a/Readme.md +++ b/Readme.md @@ -59,6 +59,8 @@ Scripts are given access to a global object that represents the graph. For C# s All methods (except `Log()`) can optionally have a `Color` value specified as the first argument for the color the drawings should be done with. If no color is specified than they will default to white. +Graphs can be animated by defining multiple frames. Each frame consists of it's own distinct set of drawn objects and each frame is its own blank canvas. A new frame can be defined by calling the `StartNextFrame()` graph method followed by drawing commands that should be run for that frame. All drawing commands called before the first `StartNextFrame()` will be drawn for the first / initial frame. By default each frame is rendered for one second, but that is changeable by calling `SetFrameTime()` on the graph and passing in the number of seconds each frame should be rendered for. + ## Scripting Languages As of right now 3 languages are supported - C#, Javascript, and Python. It is important that all script files use the correct language extension (`.cs`, `.js` and `.py` respectively), as that is how SharpPlotter knows which scripting engine to use for each file. diff --git a/SharpPlotter/GraphedItems.cs b/SharpPlotter/GraphedItems.cs index 45f8fc4..bdfe867 100644 --- a/SharpPlotter/GraphedItems.cs +++ b/SharpPlotter/GraphedItems.cs @@ -11,11 +11,9 @@ namespace SharpPlotter { public class GraphedItems { - private readonly List _points = new List(); - private readonly List _segments = new List(); - private readonly List _functions = new List(); - private readonly List _arrows = new List(); - private readonly List _polygons = new List(); + private readonly List _frames; + private int _currentRenderedFrameIndex; + private double _secondsSinceCreation; public Queue Messages { get; } = new Queue(); @@ -29,8 +27,15 @@ public class GraphedItems /// public Point2d? MaxCoordinates { get; private set; } + /// + /// How long each frame should be rendered + /// + public double SecondsPerFrame { get; set; } = 1; + public GraphedItems() { + _frames = new List {new Frame()}; + // Always start with true, as if this is a new object then obviously something has changed. ItemsChangedSinceLastRender = true; } @@ -48,7 +53,7 @@ public void AddPoints(Color color, IEnumerable points) { points ??= Array.Empty(); - _points.AddRange(points.Select(x => new RenderedPoint(x, color))); + _frames[^1].Points.AddRange(points.Select(x => new RenderedPoint(x, color))); GraphItemsUpdated(); } @@ -64,7 +69,7 @@ public void AddSegments(Color color, IEnumerable points) { if (lastPoint != null) { - _segments.Add(new RenderedSegment(lastPoint.Value, point, color)); + _frames[^1].Segments.Add(new RenderedSegment(lastPoint.Value, point, color)); } lastPoint = point; @@ -80,7 +85,7 @@ public void AddFunction(Color color, Func function) { if (function == null) throw new ArgumentNullException(nameof(function)); - _functions.Add(new RenderedFunction(color, function)); + _frames[^1].Functions.Add(new RenderedFunction(color, function)); } /// @@ -88,18 +93,29 @@ public void AddFunction(Color color, Func function) /// public void AddArrow(Color color, Point2d start, Point2d end) { - _arrows.Add(new RenderedArrow(start, end, color)); + _frames[^1].Arrows.Add(new RenderedArrow(start, end, color)); GraphItemsUpdated(); } + /// + /// Adds a filled in polygon between 3 or more points + /// public void AddPolygon(Color color, IEnumerable points) { points ??= Array.Empty(); var polygon = new RenderedPolygon(color, points); - _polygons.Add(polygon); + _frames[^1].Polygons.Add(polygon); GraphItemsUpdated(); } + + /// + /// Creates a new frame to add items into + /// + public void StartNextFrame() + { + _frames.Add(new Frame()); + } /// /// Provides the list of items that should be rendered. This will reset `ItemsChangedSinceLastRender` @@ -107,18 +123,43 @@ public void AddPolygon(Color color, IEnumerable points) internal ItemsToRender GetItemsToRender() { ItemsChangedSinceLastRender = false; - return new ItemsToRender(_points, _segments, _functions, _arrows, _polygons); + var frame = _frames[_currentRenderedFrameIndex]; + + return new ItemsToRender(frame.Points, + frame.Segments, + frame.Functions, + frame.Arrows, + frame.Polygons); + } + + /// + /// Updates the graphed items based on how long it's been since the last frame. Used mainly to control + /// multiple frames + /// + internal void Update(double secondsSinceLastFrame) + { + _secondsSinceCreation += secondsSinceLastFrame; + + var totalCycleTime = _frames.Count * SecondsPerFrame; + var frameToRender = (int) (_secondsSinceCreation / totalCycleTime * _frames.Count) % _frames.Count; + + if (frameToRender != _currentRenderedFrameIndex) + { + _currentRenderedFrameIndex = frameToRender; + ItemsChangedSinceLastRender = true; + } } private void GraphItemsUpdated() { ItemsChangedSinceLastRender = true; - var allCoordinates = _points.Select(x => x.Point) - .Union(_segments.Select(x => x.Start)) - .Union(_segments.Select(x => x.End)) - .Union(_arrows.Select(x => x.Start)) - .Union(_arrows.Select(x => x.End)) + var allCoordinates = _frames.SelectMany(x => x.Points).Select(x => x.Point) + .Union(_frames.SelectMany(x => x.Segments).Select(x => x.Start)) + .Union(_frames.SelectMany(x => x.Segments).Select(x => x.End)) + .Union(_frames.SelectMany(x => x.Arrows).Select(x => x.Start)) + .Union(_frames.SelectMany(x => x.Arrows).Select(x => x.End)) + .Union(_frames.SelectMany(x => x.Polygons).SelectMany(x => x.Points)) .Distinct() .ToArray(); @@ -146,5 +187,14 @@ private void GraphItemsUpdated() MaxCoordinates = null; } } + + private class Frame + { + public List Points { get; set; } = new List(); + public List Segments { get; set; } = new List(); + public List Functions { get; set; } = new List(); + public List Arrows { get; set; } = new List(); + public List Polygons { get; set; } = new List(); + } } } \ No newline at end of file diff --git a/SharpPlotter/MonoGame/App.cs b/SharpPlotter/MonoGame/App.cs index 7599cc4..a2f9555 100644 --- a/SharpPlotter/MonoGame/App.cs +++ b/SharpPlotter/MonoGame/App.cs @@ -99,6 +99,7 @@ protected override void Update(GameTime gameTime) } } + _graphedItems?.Update(gameTime.ElapsedGameTime.TotalSeconds); base.Update(gameTime); } diff --git a/SharpPlotter/Scripting/CSharpScriptRunner.cs b/SharpPlotter/Scripting/CSharpScriptRunner.cs index cf24194..85bf551 100644 --- a/SharpPlotter/Scripting/CSharpScriptRunner.cs +++ b/SharpPlotter/Scripting/CSharpScriptRunner.cs @@ -32,6 +32,11 @@ public class CSharpScriptRunner : IScriptRunner // drawn as. The `Points()` and `Segments()` methods can be given an `Enumerable<(float, float)>` to be given // any number of points. // +// The graph can be animated by calling `Graph.StartNextFrame()`. All drawing commands called after the `StartNextFrame` +// method will be attached to a new animation frame, and previous animation frames will be un-editable. All drawing +// commands called before the first `StartNextFrame` call will be for rendering the first animation frame. The time +// each frame is displayed for defaults to 1 second, but can be changed by calling `Graph.SetFrameTime(seconds)`. +// using System; using System.Linq; @@ -331,6 +336,19 @@ public void Arrow((double x, double y) start, (double x, double y) end) new Point2d((float) start.x, (float) start.y), new Point2d((float) end.x, (float) end.y)); } + + public void StartNextFrame() + { + _graphedItems.StartNextFrame(); + } + + public void SetFrameTime(double seconds) + { + if (seconds > 0) + { + _graphedItems.SecondsPerFrame = seconds; + } + } } } } \ No newline at end of file diff --git a/SharpPlotter/Scripting/JavascriptRunner.cs b/SharpPlotter/Scripting/JavascriptRunner.cs index dfa514e..e73b643 100644 --- a/SharpPlotter/Scripting/JavascriptRunner.cs +++ b/SharpPlotter/Scripting/JavascriptRunner.cs @@ -38,6 +38,12 @@ public class JavascriptRunner : IScriptRunner // // All graph functions except `Log()` can have a first parameter being a color value to change the color of the // drawn data. +// +// The graph can be animated by calling `graph.StartNextFrame()`. All drawing commands called after the `StartNextFrame` +// method will be attached to a new animation frame, and previous animation frames will be un-editable. All drawing +// commands called before the first `StartNextFrame` call will be for rendering the first animation frame. The time +// each frame is displayed for defaults to 1 second, but can be changed by calling `Graph.SetFrameTime(seconds)`. +// ".TrimStart(); @@ -146,6 +152,19 @@ public void Arrow(object start, object end) _graphedItems.AddArrow(Color.White, startPoints[0], endPoints[0]); } + public void StartNextFrame() + { + _graphedItems.StartNextFrame(); + } + + public void SetFrameTime(double seconds) + { + if (seconds > 0) + { + _graphedItems.SecondsPerFrame = seconds; + } + } + private static (Color color, Point2d[] points) ParseObjects(params object[] objects) { // JInt will always call this as an array of objects, so we'll have to manually parse each argument out diff --git a/SharpPlotter/Scripting/PythonScriptRunner.cs b/SharpPlotter/Scripting/PythonScriptRunner.cs index 5186e06..52bb227 100644 --- a/SharpPlotter/Scripting/PythonScriptRunner.cs +++ b/SharpPlotter/Scripting/PythonScriptRunner.cs @@ -35,6 +35,11 @@ public class PythonScriptRunner : IScriptRunner # All functions except `Log()` can optionally take a color value as the first parameter to change what color each # drawing is rendered as. # +# The graph can be animated by calling `graph.StartNextFrame()`. All drawing commands called after the `StartNextFrame` +# method will be attached to a new animation frame, and previous animation frames will be un-editable. All drawing +# commands called before the first `StartNextFrame` call will be for rendering the first animation frame. The time +# each frame is displayed for defaults to 1 second, but can be changed by calling `Graph.SetFrameTime(seconds)`. +# ".TrimStart(); @@ -152,6 +157,19 @@ public void Arrow(object start, object end) _graphedItems.AddArrow(Color.White, startPoints[0], endPoints[0]); } + public void StartNextFrame() + { + _graphedItems.StartNextFrame(); + } + + public void SetFrameTime(double seconds) + { + if (seconds > 0) + { + _graphedItems.SecondsPerFrame = seconds; + } + } + private static (Color color, Point2d[] points) ParseObjects(params object[] objects) { // Due to python's dynamic nature, the incoming objects can be one of several type of objects