diff --git a/Base/Forms/GraphForm.cs b/Base/Forms/GraphForm.cs index c1e20ea..69379d8 100644 --- a/Base/Forms/GraphForm.cs +++ b/Base/Forms/GraphForm.cs @@ -1,5 +1,7 @@ using Graphing.Extensions; using Graphing.Graphables; +using Graphing.Parts; +using System.Drawing.Drawing2D; using System.Text; namespace Graphing.Forms; @@ -145,6 +147,7 @@ public partial class GraphForm : Form protected override void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics; + g.SmoothingMode = SmoothingMode.HighQuality; Brush background = new SolidBrush(Color.White); g.FillRectangle(background, e.ClipRectangle); @@ -154,19 +157,9 @@ public partial class GraphForm : Form // Draw the actual graphs. for (int i = 0; i < ables.Count; i++) { - IEnumerable lines = ables[i].GetItemsToRender(this); + IEnumerable lines = ables[i].GetItemsToRender(this); Brush graphBrush = new SolidBrush(ables[i].Color); - Pen penBrush = new(graphBrush, 3); - - foreach (Line2d l in lines) - { - if (!double.IsNormal(l.a.x) || !double.IsNormal(l.a.y) || - !double.IsNormal(l.b.x) || !double.IsNormal(l.b.y)) continue; - - Int2 start = GraphSpaceToScreenSpace(l.a), - end = GraphSpaceToScreenSpace(l.b); - g.DrawLine(penBrush, start, end); - } + foreach (IGraphPart gp in lines) gp.Render(this, g, graphBrush); } base.OnPaint(e); @@ -375,24 +368,17 @@ public partial class GraphForm : Form long total = 0; foreach (Graphable able in ables) { - if (able is Equation equ) - { - long size = equ.GetCacheBytes(); - message.AppendLine($"{able.Name}: {size.FormatAsBytes()}"); - - total += size; - } + long size = able.GetCacheBytes(); + message.AppendLine($"{able.Name}: {size.FormatAsBytes()}"); + total += size; } - message.AppendLine($"\nTotal: {total.FormatAsBytes()}\n\nClick \"No\" to erase caches."); + message.AppendLine($"\nTotal: {total.FormatAsBytes()}\n\nErase cache?"); DialogResult result = MessageBox.Show(message.ToString(), "Graph Caches", MessageBoxButtons.YesNo, MessageBoxIcon.Information); - if (result == DialogResult.No) + if (result == DialogResult.Yes) { - foreach (Graphable able in ables) - { - if (able is Equation equ) equ.EraseCache(); - } + foreach (Graphable able in ables) able.EraseCache(); } } } diff --git a/Base/Forms/SetZoomForm.cs b/Base/Forms/SetZoomForm.cs index 30e2567..1d30ee4 100644 --- a/Base/Forms/SetZoomForm.cs +++ b/Base/Forms/SetZoomForm.cs @@ -1,131 +1,119 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Linq; -using System.Numerics; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Forms; +namespace Graphing.Forms; -namespace Graphing.Forms +public partial class SetZoomForm : Form { - public partial class SetZoomForm : Form + private double minZoomRange; + private double maxZoomRange; + + private double zoomLevel; + + private readonly GraphForm form; + + public SetZoomForm(GraphForm form) { - private double minZoomRange; - private double maxZoomRange; + InitializeComponent(); - private double zoomLevel; + minZoomRange = 1 / (form.ZoomLevel * 2); + maxZoomRange = 2 / form.ZoomLevel; + zoomLevel = 1 / form.ZoomLevel; - private readonly GraphForm form; + ZoomTrackBar.Value = (int)(ZoomToFactor(zoomLevel) * (ZoomTrackBar.Maximum - ZoomTrackBar.Minimum) + ZoomTrackBar.Minimum); - public SetZoomForm(GraphForm form) + this.form = form; + } + + protected override void OnPaint(PaintEventArgs e) + { + ZoomMaxValue.Text = maxZoomRange.ToString("0.00"); + ZoomMinValue.Text = minZoomRange.ToString("0.00"); + + ValueLabel.Text = $"{zoomLevel:0.00}x"; + + base.OnPaint(e); + + form.ZoomLevel = 1 / zoomLevel; + form.Invalidate(false); + } + + private double FactorToZoom(double factor) + { + return minZoomRange + (factor * factor) * (maxZoomRange - minZoomRange); + } + private double ZoomToFactor(double zoom) + { + double sqrValue = (zoom - minZoomRange) / (maxZoomRange - minZoomRange); + return Math.Sign(sqrValue) * Math.Sqrt(Math.Abs(sqrValue)); + } + + private void ZoomTrackBar_Scroll(object? sender, EventArgs e) + { + double factor = (ZoomTrackBar.Value - ZoomTrackBar.Minimum) / (double)(ZoomTrackBar.Maximum - ZoomTrackBar.Minimum); + zoomLevel = FactorToZoom(factor); + + Invalidate(true); + } + + private void ZoomMinValue_TextChanged(object? sender, EventArgs e) + { + double original = minZoomRange; + try { - InitializeComponent(); + double value; + if (string.IsNullOrWhiteSpace(ZoomMinValue.Text) || + ZoomMinValue.Text.EndsWith('.')) + { + return; + } + else + { + value = double.Parse(ZoomMinValue.Text); + if (value < 1e-2 || value > 1e3 || value > maxZoomRange) throw new(); + } - minZoomRange = 1 / (form.ZoomLevel * 2); - maxZoomRange = 2 / form.ZoomLevel; - zoomLevel = 1 / form.ZoomLevel; - - ZoomTrackBar.Value = (int)(ZoomToFactor(zoomLevel) * (ZoomTrackBar.Maximum - ZoomTrackBar.Minimum) + ZoomTrackBar.Minimum); - - this.form = form; - } - - protected override void OnPaint(PaintEventArgs e) - { - ZoomMaxValue.Text = maxZoomRange.ToString("0.00"); - ZoomMinValue.Text = minZoomRange.ToString("0.00"); - - ValueLabel.Text = $"{zoomLevel:0.00}x"; - - base.OnPaint(e); - - form.ZoomLevel = 1 / zoomLevel; - form.Invalidate(false); - } - - private double FactorToZoom(double factor) - { - return minZoomRange + (factor * factor) * (maxZoomRange - minZoomRange); - } - private double ZoomToFactor(double zoom) - { - double sqrValue = (zoom - minZoomRange) / (maxZoomRange - minZoomRange); - return Math.Sign(sqrValue) * Math.Sqrt(Math.Abs(sqrValue)); - } - - private void ZoomTrackBar_Scroll(object? sender, EventArgs e) - { + minZoomRange = value; + ZoomTrackBar.Value = (int)Math.Clamp(ZoomToFactor(zoomLevel) * (ZoomTrackBar.Maximum - ZoomTrackBar.Minimum) + ZoomTrackBar.Minimum, ZoomTrackBar.Minimum, ZoomTrackBar.Maximum); double factor = (ZoomTrackBar.Value - ZoomTrackBar.Minimum) / (double)(ZoomTrackBar.Maximum - ZoomTrackBar.Minimum); - zoomLevel = FactorToZoom(factor); + double newZoom = FactorToZoom(factor); - Invalidate(true); + zoomLevel = newZoom; + if (newZoom != factor) Invalidate(true); } - - private void ZoomMinValue_TextChanged(object? sender, EventArgs e) + catch { - double original = minZoomRange; - try - { - double value; - if (string.IsNullOrWhiteSpace(ZoomMinValue.Text) || - ZoomMinValue.Text.EndsWith('.')) - { - return; - } - else - { - value = double.Parse(ZoomMinValue.Text); - if (value < 1e-2 || value > 1e3 || value > maxZoomRange) throw new(); - } - - minZoomRange = value; - ZoomTrackBar.Value = (int)Math.Clamp(ZoomToFactor(zoomLevel) * (ZoomTrackBar.Maximum - ZoomTrackBar.Minimum) + ZoomTrackBar.Minimum, ZoomTrackBar.Minimum, ZoomTrackBar.Maximum); - double factor = (ZoomTrackBar.Value - ZoomTrackBar.Minimum) / (double)(ZoomTrackBar.Maximum - ZoomTrackBar.Minimum); - double newZoom = FactorToZoom(factor); - - zoomLevel = newZoom; - if (newZoom != factor) Invalidate(true); - } - catch - { - minZoomRange = original; - ZoomMinValue.Text = minZoomRange.ToString("0.00"); - } + minZoomRange = original; + ZoomMinValue.Text = minZoomRange.ToString("0.00"); } + } - private void ZoomMaxValue_TextChanged(object sender, EventArgs e) + private void ZoomMaxValue_TextChanged(object sender, EventArgs e) + { + double original = maxZoomRange; + try { - double original = maxZoomRange; - try + double value; + if (string.IsNullOrWhiteSpace(ZoomMaxValue.Text) || + ZoomMaxValue.Text.EndsWith('.')) { - double value; - if (string.IsNullOrWhiteSpace(ZoomMaxValue.Text) || - ZoomMaxValue.Text.EndsWith('.')) - { - return; - } - else - { - value = double.Parse(ZoomMaxValue.Text); - if (value < 1e-2 || value > 1e3 || value < minZoomRange) throw new(); - } - - maxZoomRange = value; - ZoomTrackBar.Value = (int)Math.Clamp(ZoomToFactor(zoomLevel) * (ZoomTrackBar.Maximum - ZoomTrackBar.Minimum) + ZoomTrackBar.Minimum, ZoomTrackBar.Minimum, ZoomTrackBar.Maximum); - double factor = (ZoomTrackBar.Value - ZoomTrackBar.Minimum) / (double)(ZoomTrackBar.Maximum - ZoomTrackBar.Minimum); - double newZoom = FactorToZoom(factor); - - zoomLevel = newZoom; - if (newZoom != factor) Invalidate(true); + return; } - catch + else { - maxZoomRange = original; - ZoomMaxValue.Text = maxZoomRange.ToString("0.00"); + value = double.Parse(ZoomMaxValue.Text); + if (value < 1e-2 || value > 1e3 || value < minZoomRange) throw new(); } + + maxZoomRange = value; + ZoomTrackBar.Value = (int)Math.Clamp(ZoomToFactor(zoomLevel) * (ZoomTrackBar.Maximum - ZoomTrackBar.Minimum) + ZoomTrackBar.Minimum, ZoomTrackBar.Minimum, ZoomTrackBar.Maximum); + double factor = (ZoomTrackBar.Value - ZoomTrackBar.Minimum) / (double)(ZoomTrackBar.Maximum - ZoomTrackBar.Minimum); + double newZoom = FactorToZoom(factor); + + zoomLevel = newZoom; + if (newZoom != factor) Invalidate(true); + } + catch + { + maxZoomRange = original; + ZoomMaxValue.Text = maxZoomRange.ToString("0.00"); } } } diff --git a/Base/Graphable.cs b/Base/Graphable.cs index 1ccd839..7dc48a5 100644 --- a/Base/Graphable.cs +++ b/Base/Graphable.cs @@ -1,4 +1,5 @@ using Graphing.Forms; +using Graphing.Parts; namespace Graphing; @@ -26,5 +27,8 @@ public abstract class Graphable Name = "Unnamed Graphable."; } - public abstract IEnumerable GetItemsToRender(in GraphForm graph); + public abstract IEnumerable GetItemsToRender(in GraphForm graph); + + public abstract void EraseCache(); + public abstract long GetCacheBytes(); } diff --git a/Base/Graphables/Equation.cs b/Base/Graphables/Equation.cs index c7eb67d..e9aae99 100644 --- a/Base/Graphables/Equation.cs +++ b/Base/Graphables/Equation.cs @@ -1,4 +1,5 @@ using Graphing.Forms; +using Graphing.Parts; namespace Graphing.Graphables; @@ -19,73 +20,73 @@ public class Equation : Graphable cache = []; } - public override IEnumerable GetItemsToRender(in GraphForm graph) + public override IEnumerable GetItemsToRender(in GraphForm graph) { const int step = 10; double epsilon = Math.Abs(graph.ScreenSpaceToGraphSpace(new Int2(0, 0)).x - graph.ScreenSpaceToGraphSpace(new Int2(step / 2, 0)).x) / 5; - List lines = []; + List lines = []; - bool addedToDictionary = false; double previousX = graph.MinVisibleGraph.x; - double previousY = GetFromCache(previousX, epsilon, ref addedToDictionary); + double previousY = GetFromCache(previousX, epsilon); for (int i = 1; i < graph.ClientRectangle.Width; i += step) { double currentX = graph.ScreenSpaceToGraphSpace(new Int2(i, 0)).x; - double currentY = GetFromCache(currentX, epsilon, ref addedToDictionary); + double currentY = GetFromCache(currentX, epsilon); if (Math.Abs(currentY - previousY) <= 10) { - lines.Add(new Line2d(new Float2(previousX, previousY), new Float2(currentX, currentY))); + lines.Add(new GraphLine(new Float2(previousX, previousY), new Float2(currentX, currentY))); } previousX = currentX; previousY = currentY; } - if (addedToDictionary) cache.Sort((a, b) => a.y.CompareTo(b.y)); + // if (addedToDictionary) cache.Sort((a, b) => a.y.CompareTo(b.y)); todo: not required until binary search return lines; } public EquationDelegate GetDelegate() => equ; - public void EraseCache() => cache.Clear(); - private double GetFromCache(double x, double epsilon, ref bool addedToDictionary) + public override void EraseCache() => cache.Clear(); + private double GetFromCache(double x, double epsilon) { - (double dist, double nearest) = NearestCachedPoint(x); + (double dist, double nearest, int index) = NearestCachedPoint(x); if (dist < epsilon) return nearest; else { - addedToDictionary = true; double result = equ(x); cache.Add(new(x, result)); // TODO: Rather than sorting the whole list when we add a single number, - // we could just insert it. + // we could just insert it in its proper place. return result; } } - private (double dist, double y) NearestCachedPoint(double x) + private (double dist, double y, int index) NearestCachedPoint(double x) { // TODO: Replace with a binary search system. double closestDist = double.PositiveInfinity; double closest = 0; + int closestIndex = -1; - foreach (Float2 p in cache) + for (int i = 0; i < cache.Count; i++) { - double dist = Math.Abs(x - p.x); + double dist = Math.Abs(x - cache[i].x); if (dist < closestDist) { closestDist = dist; - closest = p.y; + closest = cache[i].y; + closestIndex = i; } } - return (closestDist, closest); + return (closestDist, closest, closestIndex); } - public long GetCacheBytes() => cache.Count * 16; + public override long GetCacheBytes() => cache.Count * 16; } public delegate double EquationDelegate(double x); diff --git a/Base/Graphables/SlopeField.cs b/Base/Graphables/SlopeField.cs index a230240..71074d3 100644 --- a/Base/Graphables/SlopeField.cs +++ b/Base/Graphables/SlopeField.cs @@ -1,4 +1,6 @@ using Graphing.Forms; +using Graphing.Parts; +using static System.Windows.Forms.LinkLabel; namespace Graphing.Graphables; @@ -9,6 +11,8 @@ public class SlopeField : Graphable private readonly SlopeFieldsDelegate equ; private readonly double detail; + private readonly List<(Float2, GraphLine)> cache; + public SlopeField(int detail, SlopeFieldsDelegate equ) { slopeFieldNum++; @@ -16,25 +20,26 @@ public class SlopeField : Graphable this.equ = equ; this.detail = detail; + cache = []; } - public override IEnumerable GetItemsToRender(in GraphForm graph) + public override IEnumerable GetItemsToRender(in GraphForm graph) { - List lines = []; + double epsilon = 1 / (detail * 2); + List lines = []; for (double x = Math.Ceiling(graph.MinVisibleGraph.x - 1); x < graph.MaxVisibleGraph.x + 1; x += 1 / detail) { for (double y = Math.Ceiling(graph.MinVisibleGraph.y - 1); y < graph.MaxVisibleGraph.y + 1; y += 1 / detail) { - double slope = equ(x, y); - lines.Add(MakeSlopeLine(new Float2(x, y), slope)); + lines.Add(GetFromCache(epsilon, x, y)); } } return lines; } - private Line2d MakeSlopeLine(Float2 position, double slope) + private GraphLine MakeSlopeLine(Float2 position, double slope) { double size = detail; @@ -46,6 +51,28 @@ public class SlopeField : Graphable return new(new(position.x + dirX, position.y + dirY), new(position.x - dirX, position.y - dirY)); } + private GraphLine GetFromCache(double epsilon, double x, double y) + { + // Probably no binary search here, though maybe it could be done + // in terms of just one axis. + + foreach ((Float2 p, GraphLine l) in cache) + { + double diffX = Math.Abs(p.x - x), + diffY = Math.Abs(p.y - y); + + if (diffX < epsilon && diffY < epsilon) return l; + } + + // Create a new value. + double slope = equ(x, y); + GraphLine result = MakeSlopeLine(new Float2(x, y), slope); + cache.Add((new Float2(x, y), result)); + return result; + } + + public override void EraseCache() => cache.Clear(); + public override long GetCacheBytes() => cache.Count * 48; } public delegate double SlopeFieldsDelegate(double x, double y); diff --git a/Base/Graphables/TangentLine.cs b/Base/Graphables/TangentLine.cs new file mode 100644 index 0000000..43e3739 --- /dev/null +++ b/Base/Graphables/TangentLine.cs @@ -0,0 +1,47 @@ +using Graphing.Forms; +using Graphing.Parts; + +namespace Graphing.Graphables; + +public class TangentLine : Graphable +{ + public double Position { get; set; } + + private readonly EquationDelegate parentEqu; + + private readonly double length; + + public TangentLine(double length, double position, Equation parent) + { + Name = $"Tangent Line of {parent.Name}"; + + parentEqu = parent.GetDelegate(); + Position = position; + this.length = length; + } + + public override IEnumerable GetItemsToRender(in GraphForm graph) + { + Float2 point = new(Position, parentEqu(Position)); + return [MakeSlopeLine(point, DerivativeAtPoint(Position)), + new GraphCircle(point, 8)]; + } + private GraphLine MakeSlopeLine(Float2 position, double slope) + { + double dirX = length, dirY = slope * length; + double magnitude = Math.Sqrt(dirX * dirX + dirY * dirY); + + dirX /= magnitude * 2 / length; + dirY /= magnitude * 2 / length; + + return new(new(position.x + dirX, position.y + dirY), new(position.x - dirX, position.y - dirY)); + } + private double DerivativeAtPoint(double x) + { + const double step = 1e-3; + return (parentEqu(x + step) - parentEqu(x)) / step; + } + + public override void EraseCache() { } + public override long GetCacheBytes() => 0; +} diff --git a/Base/IGraphPart.cs b/Base/IGraphPart.cs new file mode 100644 index 0000000..0e3c592 --- /dev/null +++ b/Base/IGraphPart.cs @@ -0,0 +1,8 @@ +using Graphing.Forms; + +namespace Graphing; + +public interface IGraphPart +{ + public void Render(in GraphForm form, in Graphics g, in Brush brush); +} diff --git a/Base/Line2d.cs b/Base/Line2d.cs deleted file mode 100644 index 8103f58..0000000 --- a/Base/Line2d.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Graphing; - -public record struct Line2d -{ - public Float2 a; - public Float2 b; - - public Line2d() - { - a = new(); - b = new(); - } - public Line2d(Float2 a, Float2 b) - { - this.a = a; - this.b = b; - } -} diff --git a/Base/Parts/GraphCircle.cs b/Base/Parts/GraphCircle.cs new file mode 100644 index 0000000..a5154c6 --- /dev/null +++ b/Base/Parts/GraphCircle.cs @@ -0,0 +1,31 @@ +using Graphing.Forms; + +namespace Graphing.Parts; + +public record struct GraphCircle : IGraphPart +{ + public Float2 center; + public int radius; + + public GraphCircle() + { + center = new(); + radius = 1; + } + public GraphCircle(Float2 center, int radius) + { + this.center = center; + this.radius = radius; + } + + public readonly void Render(in GraphForm form, in Graphics g, in Brush brush) + { + if (!double.IsNormal(center.x) || !double.IsNormal(center.y) || + !double.IsNormal(radius)) return; + + Int2 centerPix = form.GraphSpaceToScreenSpace(center); + g.FillEllipse(brush, new Rectangle(new Point(centerPix.x - radius, + centerPix.y - radius), + new Size(radius * 2, radius * 2))); + } +} diff --git a/Base/Parts/GraphLine.cs b/Base/Parts/GraphLine.cs new file mode 100644 index 0000000..a185f5c --- /dev/null +++ b/Base/Parts/GraphLine.cs @@ -0,0 +1,32 @@ +using Graphing.Forms; + +namespace Graphing.Parts; + +public record struct GraphLine : IGraphPart +{ + public Float2 a; + public Float2 b; + + public GraphLine() + { + a = new(); + b = new(); + } + public GraphLine(Float2 a, Float2 b) + { + this.a = a; + this.b = b; + } + + public readonly void Render(in GraphForm form, in Graphics g, in Brush brush) + { + if (!double.IsNormal(a.x) || !double.IsNormal(a.y) || + !double.IsNormal(b.x) || !double.IsNormal(b.y)) return; + + Int2 start = form.GraphSpaceToScreenSpace(a), + end = form.GraphSpaceToScreenSpace(b); + + Pen pen = new(brush, 3); + g.DrawLine(pen, start, end); + } +} diff --git a/Testing/Program.cs b/Testing/Program.cs index 40a670b..5aec97a 100644 --- a/Testing/Program.cs +++ b/Testing/Program.cs @@ -10,12 +10,21 @@ internal static class Program { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); - Application.SetHighDpiMode(HighDpiMode.PerMonitorV2); + Application.SetHighDpiMode(HighDpiMode.SystemAware); GraphForm graph = new("One Of The Graphing Calculators Of All Time"); - graph.Graph(new Equation(x => Math.Pow(2, x))); - graph.Graph(new Equation(Math.Log2)); + + Equation equ = new(x => x * x); + TangentLine tangent = new(5, 2, equ); + + graph.Graph(equ); + graph.Graph(tangent); Application.Run(graph); } + + private static double PopulationGraph(double max, double k, double A, double t) + { + return max / (1 + A * Math.Exp(-k * t)); + } }