Hi
da des Öfteren mal eine Frage zum Plotten von Funktionen gestellt wird, habe ich mal einen einfachen Algorithmus dafür geschrieben. Im Prinzip wird immer der Mittelwert zwischen zwei Werten genommen und in die Funktion eingesetzt. Anschließend wird jeweils überprüft, ob der Abstand zum 1. bzw. 2. Wert größer dem übergebenen delta ist und, falls zutreffend, die Prozedur erneut aufgerufen. Dadurch ist eine erheblich größere Genauigkeit gegeben, als durch einen immer gleichbleibenden Abstand zwischen den zu zeichnenden Punkten und DrawLine.
Als Anmerkung vorweg: Der Algorithmus ist bei Weitem nicht perfekt. Bei vertikalen Asymptoten und häufig auch bei unstetigen Funktionen hängt er sich bspw. auf, bei kleinen Abständen (< delta) bricht er das Zeichnen bereits vorher ab. Dies liegt vor allem daran, dass durch die Verwendung des Delegatens quasi keine Analyse auf die Asymptoten, sowie somit das Verhalten an diesen möglich ist. Kleine Abstände werden hingegen einfach übersprungen, da eine Analyse auf lokale Extremwerte ebenfalls nicht möglich ist. Insgesamt ist der Quellcode auch eigentlich nicht auf diesen einfachen Fall zugeschnitten, daher bitte ich über etwaige Unsauberkeiten und Unschönheiten hinwegzusehen. Das Verfahren selbst ist extrem stapellastig, daher kann es zu Stapelüberläufen kommen.
Spoiler anzeigen
Beispiel für Aufruf:
Viel Spaß.
Gruß
~blaze~
da des Öfteren mal eine Frage zum Plotten von Funktionen gestellt wird, habe ich mal einen einfachen Algorithmus dafür geschrieben. Im Prinzip wird immer der Mittelwert zwischen zwei Werten genommen und in die Funktion eingesetzt. Anschließend wird jeweils überprüft, ob der Abstand zum 1. bzw. 2. Wert größer dem übergebenen delta ist und, falls zutreffend, die Prozedur erneut aufgerufen. Dadurch ist eine erheblich größere Genauigkeit gegeben, als durch einen immer gleichbleibenden Abstand zwischen den zu zeichnenden Punkten und DrawLine.
Als Anmerkung vorweg: Der Algorithmus ist bei Weitem nicht perfekt. Bei vertikalen Asymptoten und häufig auch bei unstetigen Funktionen hängt er sich bspw. auf, bei kleinen Abständen (< delta) bricht er das Zeichnen bereits vorher ab. Dies liegt vor allem daran, dass durch die Verwendung des Delegatens quasi keine Analyse auf die Asymptoten, sowie somit das Verhalten an diesen möglich ist. Kleine Abstände werden hingegen einfach übersprungen, da eine Analyse auf lokale Extremwerte ebenfalls nicht möglich ist. Insgesamt ist der Quellcode auch eigentlich nicht auf diesen einfachen Fall zugeschnitten, daher bitte ich über etwaige Unsauberkeiten und Unschönheiten hinwegzusehen. Das Verfahren selbst ist extrem stapellastig, daher kann es zu Stapelüberläufen kommen.
VB.NET-Quellcode
- <System.Runtime.InteropServices.DllImport("gdi32.dll")> _
- Private Shared Function SetPixel(hdc As IntPtr, x As Integer, y As Integer, argb As Integer) As Integer
- End Function
- <System.Runtime.InteropServices.DllImport("gdi32.dll")> _
- Private Shared Function GetPixel(hdc As IntPtr, x As Integer, y As Integer) As Integer
- End Function
- ''' <summary>
- ''' Kapselt eine Funktion mit einem Parameter vom Typ
- ''' <see cref="System.Double"/>
- ''' und einem Rückgabewert vom Typ
- ''' <see cref="System.Double"/>.
- ''' </summary>
- ''' <param name="value">Der Wert, der ausgewertet wird.</param>
- ''' <returns>Der von der gekapselten Funktion zurückgegebene Wert.</returns>
- Public Delegate Function Func(value As Double) As Double
- ''' <summary>
- ''' Stellt ein von zwei Werten begrenztes Intervall dar.
- ''' </summary>
- Public Structure Range
- Private _first As Double, _last As Double
- ''' <summary>
- ''' Gibt den ersten Wert zurück.
- ''' </summary>
- Public ReadOnly Property First() As Double
- Get
- Return _first
- End Get
- End Property
- ''' <summary>
- ''' Gibt den zweiten Endwert zurück.
- ''' </summary>
- Public ReadOnly Property Last() As Double
- Get
- Return _last
- End Get
- End Property
- ''' <param name="first">Der erste Wert des Intervalls.</param>
- ''' <param name="last">Der zweite Wert des Intervalls.</param>
- Public Sub New(first As Double, last As Double)
- _first = first
- _last = last
- End Sub
- End Structure
- ''' <summary>
- ''' Zeichnet die angegebene Funktion auf das Ziel.
- ''' </summary>
- ''' <param name="surface">Die Oberfläche, auf die gezeichnet werden soll.</param>
- ''' <param name="color">Die Darstellungsfarbe der dargestellten Funktion.</param>
- ''' <param name="xRange">Die zu zeichnende Definitionsmenge.</param>
- ''' <param name="yRange">Die zu zeichnende Wertemenge.</param>
- ''' <param name="bounds">Der Bereich, in dem die Funktion dargestellt werden soll.</param>
- ''' <param name="origin">Der Koordinatenursprung.</param>
- ''' <param name="evaluatedFunction">Die zu zeichnende Funktion.</param>
- ''' <remarks>
- ''' Funktionen mit asymptotischem Verhalten innerhalb eines zu zeichnenden Intervalls müssen
- ''' vor dem Zeichnen aufgespalten werden und nacheinander gezeichnet werden.
- ''' </remarks>
- Public Shared Sub Plot(surface As Graphics, color As Color, xRange As Range, yRange As Range, bounds As RectangleF, origin As PointF, _
- evaluatedFunction As Func)
- Plot(surface, color, xRange, yRange, bounds, origin, _
- 1.0, evaluatedFunction)
- End Sub
- ''' <summary>
- ''' Zeichnet die angegebene Funktion auf das Ziel.
- ''' </summary>
- ''' <param name="surface">Die Oberfläche, auf die gezeichnet werden soll.</param>
- ''' <param name="color">Die Darstellungsfarbe der dargestellten Funktion.</param>
- ''' <param name="xRange">Die zu zeichnende Definitionsmenge.</param>
- ''' <param name="yRange">Die zu zeichnende Wertemenge.</param>
- ''' <param name="bounds">Der Bereich, in dem die Funktion dargestellt werden soll.</param>
- ''' <param name="origin">Der Koordinatenursprung.</param>
- ''' <param name="delta">Der minimale Abstand zwischen zwei Werten, für die eine weitere Zeichenoperation durchgeführt werden soll.</param>
- ''' <param name="evaluatedFunction">Die zu zeichnende Funktion.</param>
- ''' <remarks>
- ''' Funktionen mit asymptotischem Verhalten innerhalb eines zu zeichnenden Intervalls müssen
- ''' vor dem Zeichnen aufgespalten werden und nacheinander gezeichnet werden.
- ''' </remarks>
- Public Shared Sub Plot(surface As Graphics, color As Color, xRange As Range, yRange As Range, bounds As RectangleF, origin As PointF, _
- delta As Double, evaluatedFunction As Func)
- Dim prev As Region = surface.Clip
- Dim cur As New Region(bounds)
- Dim lv As Double, rv As Double
- cur.Intersect(prev)
- 'Zu fuellenden Bereich beschraenken
- surface.Clip = cur
- 'linker y-Wert
- lv = evaluatedFunction(xRange.First)
- 'rechter y-Wert
- rv = evaluatedFunction(xRange.Last)
- 'Verschiebung in x- und y-Richtung berechnen
- Dim xoffs As Single = bounds.Left + origin.X - bounds.Width * CSng(xRange.First / (xRange.Last - xRange.First))
- Dim yoffs As Single = bounds.Bottom + origin.Y + bounds.Height * CSng(yRange.First / (yRange.Last - yRange.First))
- 'Streckfaktor in x- und y-Richtung berechnen
- Dim xfct As Double = bounds.Width / (xRange.Last - xRange.First)
- Dim yfct As Double = -bounds.Height / (yRange.Last - yRange.First)
- 'Positionen von linkem und rechten Wert berechnen
- Dim lpos As New PointF(CSng(xRange.First * xfct) + xoffs, CSng(lv * yfct) + yoffs)
- Dim rpos As New PointF(CSng(xRange.Last * xfct) + xoffs, CSng(rv * yfct) + yoffs)
- 'Intervall zwischen den beiden Werten berechnen
- PlotInterval(surface.GetHdc(), color.ToArgb(), xRange.First, xRange.Last, lv, rv, _
- lpos, rpos, xfct, xoffs, yfct, yoffs, _
- delta, evaluatedFunction)
- surface.ReleaseHdc()
- 'Linken und rechten Punkt zeichnen
- surface.FillRectangle(Brushes.Black, lpos.X, lpos.Y, 1F, 1F)
- surface.FillRectangle(Brushes.Black, rpos.X, rpos.Y, 1F, 1F)
- 'urspruenglichen zu fuellenden Bereich wieder uebernehmen
- surface.Clip = prev
- cur.Dispose()
- End Sub
- Private Shared Sub PlotInterval(hdc As IntPtr, color As Integer, l As Double, r As Double, lv As Double, rv As Double, _
- lpos As PointF, rpos As PointF, xfct As Double, xoffs As Single, yfct As Double, yoffs As Single, _
- delta As Double, func As Func)
- 'Mittle vom linken x-Wert (l) und rechten x-Wert (r) ermitteln
- Dim m As Double = (l + r) / 2.0
- 'y-Wert des x-Wertes berechnen
- Dim mv As Double = func(m)
- 'Abstand zwischen den beiden Punkten ermitteln
- Dim dx As Double = (m - l) * xfct, dy As Double = (mv - lv) * yfct
- 'zu zeichnende Position des mittleren Werts ermitteln
- Dim pos As New PointF(CSng(m * xfct) + xoffs, CSng(mv * yfct) + yoffs)
- 'Falls der Abstand groesser, als der maximale Abstand ist, wird der Wert zwischen den beiden Werten ebenfalls gezeichnet, sonst ausgelassen.
- If dx * dx + dy * dy >= delta Then
- PlotInterval(hdc, color, l, m, lv, mv, _
- lpos, pos, xfct, xoffs, yfct, yoffs, _
- delta, func)
- End If
- 'selbiges fuer den rechten Teil
- dx = (r - m) * xfct
- dy = (rv - mv) * yfct
- If dx * dx + dy * dy >= delta Then
- PlotInterval(hdc, color, r, m, rv, mv, _
- pos, rpos, xfct, xoffs, yfct, yoffs, _
- delta, func)
- End If
- 'Pixel darstellen
- DrawPixel(hdc, color, pos.X, pos.Y)
- End Sub
- Private Shared Sub DrawPixel(hdc As IntPtr, color As Integer, x As Single, y As Single)
- Dim xl As Integer = CInt(Math.Truncate(Math.Floor(x)))
- Dim yl As Integer = CInt(Math.Truncate(Math.Floor(y)))
- Dim fctx As Single = x - xl
- Dim fcty As Single = y - yl
- 'Einfache "billige" Antialisierung anwenden
- SetPixel(hdc, xl, yl, BlendPixelBgr(GetPixel(hdc, xl, yl), color, (1F - fctx) * (1F - fcty)))
- SetPixel(hdc, xl + 1, yl, BlendPixelBgr(GetPixel(hdc, xl + 1, yl), color, fctx * (1F - fcty)))
- SetPixel(hdc, xl, yl + 1, BlendPixelBgr(GetPixel(hdc, xl, yl + 1), color, (1F - fctx) * fcty))
- SetPixel(hdc, xl + 1, yl + 1, BlendPixelBgr(GetPixel(hdc, xl + 1, yl + 1), color, fctx * fcty))
- End Sub
- Private Shared Function BlendPixelBgr(backbgr As Integer, argb As Integer, alphafactor As Single) As Integer
- Dim a As Integer = CInt(Math.Truncate(((argb >> 24) And &Hff) * alphafactor))
- If a = 0 Then
- 'fore-color ist nicht sichtbar
- Return backbgr
- ElseIf a = 255 Then
- 'back-color ist unsichtbar ==> r und b vertauschen
- Return ((argb And &Hff) << 16) Or (argb And &Hff00) Or ((argb And &Hff0000) >> 16)
- Else
- 'sonst einfach Alpha-Blending anwenden
- Return ((a * (argb And &Hff) + (255 - a) * ((backbgr >> 16) And &Hff)) And &Hff00) << 8 Or ((a * ((argb >> 8) And &Hff) + (255 - a) * ((backbgr >> 8) And &Hff)) And &Hff00) Or ((a * ((argb >> 16) And &Hff) + (255 - a) * (backbgr And &Hff)) And &Hff00) >> 8
- End If
- End Function
Beispiel für Aufruf:
Viel Spaß.
Gruß
~blaze~
Dieser Beitrag wurde bereits 4 mal editiert, zuletzt von „~blaze~“ ()