guest (95.37.151.99)
Aug 9, 2010
=Graphics 101 – plotting a function=
- by tsh73
- {$creationdate}
[[toc]]
----
==Goal==
Once upon a time there was a person who wanted to plot some graphs. They knew their math well enough, but had never draw a thing in Basic. Of course they looked at the help file but just became confused. Well, no wonder, the help file is a reference document. So I thought I would write a tutorial, starting from opening the graphics window through to plotting pixels, drawing points, lines and maybe adding text for labeling.
==Non-goals==
I am not going to discuss GUI or animation or sprites – I just going to teach how to draw a static picture. Namely, to plot a function.
==Pre-requisites==
I assume that you have a rudimentary working knowledge of Liberty BASIC, that you are familiar with the mainwin and can calculate, check conditions, loop and print results.
==Where to draw==
So prior to this you worked in mainwin. It is nice but it is not designed for drawing, only to print text. So, we need something to draw upon.
There are two controls that allow drawing, a window opened for graphics, and a graphicbox established within a window. You can have several graphicboxes on a window and draw in all of them. Pretty cool, but for now we take the simplest course, a window of type graphics –
[[code format="vbnet"]]
open "test" for graphics as #gr
#gr "trapclose [quit]"
wait
[quit]
close #gr
end
[[code]]
That's about as simple as it gets.
The first line opens a window of type graphics, titled "test", we can draw on that. On my computer, it's about 300x300 pixels. You can try graphics_nsb_nf instead of graphics and get an even simpler window. The second line creates the trapclose handler and names a [branch label] that will get called if the window is closed. The third line prevents our GUI from closing immediately, instead it sits waiting for user input.
The last lines are called if the window is closed either by a mouse click or Alt-F4 being pressed. They explicitly close the window and end the program. If you don't do this you will get an error report when the program stops.
If you do not need the mainwin in view you can suppress it by typing "nomainwin" at the top of your program. You might want to enable it occasionally to print variables to for debugging but generally speaking when you run a GUI you will suppress mainwin. If you need more space, you set WindowWidth and WindowHeight before opening the window. Easy!
==Pixels and coordinates==
Ahhh pixels, exactly!
They are just dots on your monitor. Anything you see on a monitor is composed of colored dots, named pixels. A pixel is the smallest element of any picture.
At this moment your monitor is showing pixels in it's current resolution, say 1280x1024. That is the size of the screen in pixels, (it can be changed ) and is the maximum size of the window you can show and draw upon.
Each pixel has coordinates. Basically that means they are numbered from 0, left to right and top to bottom. With whole numbers – there are no pixels between 0 and 1.
[[image:graph_tutorial_p1.gif]]
In math they teach another coordinate plane, there axes go from left to right and from bottom to top. And plenty of space is envisioned between whole points.
[[image:graph_tutorial_p2.gif]]
We are going to respect that then plotting functions.
==Set a Pixel==
So. We know that a screen consist of pixels, and pixels have coordinates.
So all we should actually do to draw a pixel – make a command and give coordinates?
Almost:
[[code format="vbnet"]]
nomainwin
open "test" for graphics_nsb_nf as #gr
#gr "trapclose [quit]"
#gr "down"
#gr "set 100 100"
wait
[quit]
close #gr
end
[[code]]
As you see, we need to "put pen down" before drawing. (Unfortunate anachronism I might add).
So we set a single pixel to black at 100, 100 coordinates. That is 100 pixels to the right and down from top-left corner of window. (More exactly, from top-left corner of window client area – it's "working zone").
==**Set a bunch of Pixels**==
A single point is barely visible. Let's make a bunch, shall we?
But how are we going to do it? Why, with loops. FOR loops will do just fine.
Let X change from 100 to 200, and Y equal to X.
But how do we put these numbers inside "set" command?
Let's see, literal line would look like this,
> {{#gr "set 100 100"}}
>
if we use variables we must preserve spaces,
> {{X=100: Y=100}}
> {{#gr "set ";X; " ";Y}}
(Handy tip: If your drawing command does not work as expected, turn them in to ordinary PRINT statements (by replacing #gr with print) and examine the line in the mainwin.)
So here's our program:
[[code format="vbnet"]]
nomainwin
open "test" for graphics_nsb_nf as #gr
#gr "trapclose [quit]"
#gr "down"
for X=100 to 200
Y=X
#gr, "set ";X;" ";Y
next
wait
[quit]
close #gr
end
[[code]]
==Why, it's a line! – a line==
Indeed. We just drew a line. I'm pretty sure there must be more straightforward ways to draw straight lines?
Sure there are several. But most straightforward is
> {{#gr "line 100 100 200 200"}}
>
The numbers are coordinates of first and second points, respectively.
You can also place a first point with a dedicated command and then draw from this point to another one. That comes in handy then we need to draw point-by-point chain of lines:
> {{#gr "place 100 100"}}
> {{#gr "goto 200 200"}}
if we use variables we must preserve spaces in command lines. (It applies EVERYWHERE).
==Why, it's a circle! – circle==
To easy? Let's try something fancy, with SINE and COSINE:
[[code format="vbnet"]]
nomainwin
open "test" for graphics_nsb_nf as #gr
#gr "trapclose [quit]"
#gr "down"
pi=acs(-1)
for t=0 to 2*pi step 0.01
X=100*cos(t)+150
Y=100*sin(t)+150
#gr, "set ";X;" ";Y
next
wait
[quit]
close #gr
end
[[code]]
What? A pretty round circle?
Let’s see why. It’s all math, you know.
We have that Pi number, equal 3.1415926… but much simpler to put pi=acs(-1) instead. If we change angle (t) from 0 to 2Pi (full circle, but measured in radians. Computers work in radians... that still converts to ordinary 0..360 degrees full circle, by multiplying by 180/Pi) then point (cos(t), sin(t)) will go along unit circle (circle with radius=1 and center (0,0) ) on coordinate plain.
All we added was scaling that to radius 100 (by multiplying) and shift it's center from (0,0) to (150, 150) (by adding).
And of course there is easier way to draw a circle. Just place center point, then command to draw circle with desired radius:
> {{#gr "place 150 150"}}
> {{#gr "circle 100"}}
==But bunch of dots capable of more! (Archimedes spiral)==
So far our examples looked too complex for task solved (things drawn). And there always was another, simpler way.
Do we need plotting point by point at all?
Why, sure we are. Look at this modification of last program. This beauty is called Archimedes' spiral (and actually this is example of polar plot, that is, plot of function in polar coordinates. Never mind.)
[[code format="vbnet"]]
nomainwin
open "test" for graphics_nsb_nf as #gr
#gr "trapclose [quit]"
#gr "down"
pi=acs(-1)
nLoops=10
for t=0 to 2*pi*nLoops step 0.01
X=100*t/(2*pi*nLoops)*cos(t)+150
Y=100*t/(2*pi*nLoops)*sin(t)+150
#gr, "set ";X;" ";Y
next
wait
[quit]
close #gr
end
[[code]]
==Turtle was here (and we need some stuff eventually)==
Actually, besides “line, point” approach there are another one.
Turtle graphics.
The stuff like “pen down, go 30, pen up, go 20, turn 30, pen down, go 10”. LB supports it, too. It allows to easily draw stuff like this:
[[code format="vbnet"]]
nomainwin
open "test" for graphics_nsb_nf as #gr
#gr "trapclose [quit]"
#gr "down"
#gr "place 150 250" 'initial position found by trial and error
for i=1 to 30
#gr "go 200"
#gr "turn ";180+15 '180 degrees means turn backwards.
'So, backwards and some
next
wait
[quit]
close #gr
end
[[code]]
I just think it's not well suited to plotting functions. (But it nicely covered in LB/JB tutorial, so look there in need). We'll need one thing, though.
Here's one trick I'll show you. Then we created window, I said it's approximately 300x300 pixels. But how do we measure it?
There is a command that places pen in the center of a drawing area. And another command that takes current coordinates into pair of variables. Then you double these variables, you'll get width and height of drawing area!
> {{#gr "home"}}
> {{#gr "posxy w h"}}
> {{width=2*w: height=2*h}}
So "approximately 300x300 pixels" turned out to be 312x332.
==Oh, and we was going to plot a function? A sine may be?==
First, recall some math.
Let us say, we want to plot sine from –Pi to Pi, that is, full period. And we know that sine goes from -1 to 1, no more. So we have "logical" coordinates by X in range [-3.14, 3.14] and by Y in range [-1,1].
This should be mapped on "physical" coordinates – to actual pixels, starting from (0,0) and going approximately to (300,300). Some languages provide automatic translation; we have to do it ourselves. Do not worry, it's easy.
In our case, for X coordinate, we should move X range to 0: X+3.14, and then stretch that range (2*Pi roughly makes 6) to 300 pixels: (X+3.14)*50 (approximately).
Same apply to Y: Y+1, (Y+1)*150.
But Y axis on computer is inverted, so we should do this: 300-(Y+1)*150.
Let's try that.
[[code format="vbnet"]]
nomainwin
open "test" for graphics_nsb_nf as #gr
#gr "trapclose [quit]"
#gr "down"
pi=acs(-1)
for X=0-pi to pi step 0.01
Y=sin(X)
#gr "set ";(X+3.14)*50;" ";300-(Y+1)*150
next
wait
[quit]
close #gr
end
[[code]]
Wow. The sine wave all right, with proper orientation.
If you don't like to see gaps between dots, you can connect dots by using "goto" instead of "set":
> {{#gr "goto ";(X+3.14)*50;" ";300-(Y+1)*150}}
Though, with that guesswork, we'll have hard time trying to place axes right…
==No, I mean any function (scaling goes here)==
Let's take "any" function.
Let us use f(x)= 1.5*x^2-2*sin(5*x), x in [-2,3].
So, to draw any function easily and be able to place axes, we need to eliminate the guesswork. Let’s build some formula, then use it to uniformly translate logical (math) coordinates to physical (screen) ones.
In general form:
To translate X from interval [0,1] to [c,d] we'll do X*(d-c)+c.
To translate X from interval [a,b] to [0,1] we'll do (X-a)/(b-a).
Combining, we'll get universal formula:
**To translate X from interval [a,b] to [c,d] we'll do (X-a)/(b-a)*(d-c)+c.**
We are going to use it to translate math coordinates to screen coordinates, so [a,b] is math range and [c,d] is screen one. But we can use it backwards, for example if we want to translate mouse clicks to math coordinates.
For ease of use, I suggest that we'll create two functions: sx(x) and sy(y), that'll convert math X to screen X and math Y to screen Y, respectively. But – our formula needs bunch of other numbers? No problem – all that numbers (a,b,c,d) are just range boundaries, and will not change. So we make them global just to save typing (lot's of it).
So, first global variables would be size of drawing area, let's name it winW, winH.
Other globals would be X range, name it xmin, xmax, and Y range, name it ymin, ymax.
Normally then plotting we know X range. But there to get Y range?
To get it, we just step through X range with same step we'll be plotting our graph and calculate ymin, ymax. Of course that would mean we calculate f(x) twice – first for getting Y range, second for actual plotting – but computers are pretty fast now.
One last thing before program. The coordinate axes are just two lines, that goes through point (0,0).
[[code format="vbnet"]]
nomainwin
global winW, winH, xmin, xmax, ymin, ymax
open "test" for graphics_nsb_nf as #gr
#gr "trapclose [quit]"
#gr "down"
#gr "home"
#gr "posxy w h"
winW=2*w: winH=2*h
'f(x)=1.5*x^2-2*sin(5*x), x in [-2,3]
xmin=-2: xmax=3
nPoints=winW 'we have only this much screen dots in X range
dx=(xmax-xmin)/nPoints 'so this will be step in math coordinates
'now, to get ymin, ymax we have to loop
ymin=f(xmin)
ymax=ymin
for x=xmin to xmax step dx
y=f(x)
if ymin > y then ymin = y
if ymax < y then ymax = y
next
'now we just - plot function. Note same loop
y=f(xmin)
#gr "set ";sx(xmin);" ";sy(y) 'just set first dot
for x=xmin to xmax step dx
y=f(x)
#gr "goto ";sx(x);" ";sy(y) 'then connect dots
next
'and finally, add axis
#gr "line ";sx(xmin);" ";sy(0);" ";sx(xmax);" ";sy(0)
#gr "line ";sx(0);" ";sy(ymin);" ";sx(0);" ";sy(ymax)
wait
[quit]
close #gr
end
'"any" function. You can change it as you like
function f(x)
f=1.5*x^2-2*sin(5*x)
end function
'To translate X from interval [a,b] to [c,d] we'll do (X-a)/(b-a)*(d-c)+c.
'create two functions: sx(x) and sy(y)
function sx(x)
sx=(x-xmin)/(xmax-xmin)*winW
end function
function sy(y)
sy=winH-(y-ymin)/(ymax-ymin)*winH 'Y is inverted, so winH-...
end function
[[code]]
Damn. This thing got a bit long, but I hope still understandable.
==Adding text labeling==
That's easy. You place the pen, then print "\" and the text.
Text placed top-left corner from the pen position.
If you print another time, output will be stacked under first one.
Just experiment a little.
[[code format="vbnet"]]
nomainwin
open "test" for graphics_nsb_nf as #gr
#gr "trapclose [quit]"
#gr "down"
#gr "place 100 100"
#gr "\Hello"
#gr "\from Liberty BASIC"
wait
[quit]
close #gr
end
[[code]]
For labeling a graph, you can use our translating functions sx(), sy().
So, for our graph, it could be like that:
[[code format="vbnet"]]
'labeling
#gr "place ";sx(0)+5;" ";sy(0)-5
#gr "\0,0"
#gr "place ";sx(xmax)-20;" ";sy(0)-5
#gr "\X"
#gr "place ";sx(0)+5;" ";sy(ymax)+20
#gr "\Y"
[[code]]
And if you are going really fancy, you can change font, size and style. See in a help file.
==Adding color and thickness==
As easy as could get. You just command "color red" and "size 3",
> {{#gr "color red"}}
> {{#gr "size 3"}}
(for the list of colors, look in a help file. Or just experiment).
And all points and lines you'll draw after will be red and 3 pixels thick. But you can turn it back of course:
> {{#gr "color black"}}
> {{#gr "size 1"}}
==Flush that thing down… err… I mean, stick it==
For now you probably encountered that strange thing – then you run a program, it'll draw fine, but if you try to cover that window with another or do minimize/restore, it returns empty? Alas, that's not a bug – it's a feature. But the remedy is simple: just issue command "flush" after drawing. (You probably will want to do it in the end – each flush takes memory)
[[code format="vbnet"]]
nomainwin
open "Try to cover me" for graphics_nsb_nf as #gr
#gr "trapclose [quit]"
#gr "down"
#gr "place 50 100"
#gr "\I am flushed - I'll stay"
#gr "flush"
#gr "\I am NOT flushed - I'll disappear"
wait
[quit]
close #gr
end
[[code]]
==Oh, and we could save that nice graph, do we?==
Why, yes. There couple of commands just for that.
First one is getbmp, that takes picture from your graphics window. Later it could be drawn with drawbmp, or saved with bmpsave.
For our graph, it will be
> {{#gr, "getbmp drawing 1 1 ";winW;" ";winH}}
> {{bmpsave "drawing", "graph.bmp"}}
Now you have this picture saved as graph.bmp in the directory with your program.
Here what we'll got:
[[image:graph_tutorial_graph.gif]]
==Where to get more==
Why, if you have read this and want more, may be it's time to look back into a help file? I hope it will make more sense now ;)
==Addendum==
Whole program (just in case you missed something):
[[graph_tutorial.bas]]