=Graphics 101 – plotting a function= - by tsh73 - {$creationdate} [[toc]] ---- ==Goal== (and history). OnceOnce upon a time there was a person who wanted to plot some graphs. He supposedlyThey knew histheir math well,well enough, but justhad never draw a thing in Basic. Of course he was directed to athey looked at the help file;file but he said "I have read through the help files on graphics and it just confuses me".became confused. Well, no wonder –wonder, the help file is a reference in a first place, so it's more or less OK to dump all graphic functions into one BIG page *shrugs*. There is tutorial in LB, but it surprisingly focuses on turtle graphics, and leaves plotting for the reader so to say. Sodocument. So I thought I could as wellwould write tutorial myself, with intenta tutorial, starting from opening the graphics widow through to cover "starting from open for graphics, viaplotting pixels, coordinates, pointsdrawing points, lines and lines. I guess that's all really needed, may be add printingmaybe adding text for labeling and flush." So here I am. Forgive my Clatchian English. I suppose you run LB (JB will do just fine) along as you read this tutorial.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 supposeassume that you already are "masterhave a rudimentary working knowledge of mainwin". YouLiberty BASIC, that you are familiar with the mainwin and can calculate, check conditions, loop and print results. These are really basics, so if not, go to part one of LB / JB tutorial. ==Where to draw== So, supposeSo prior to this you worked in mainwin. It is nice but it is not designed to draw stuff on –for drawing, only to print text. So, we need something to draw upon. There There are two types of thingscontrols that allow drawing on itself – it'sdrawing, a window opened for graphics, and a graphicbox added to ordinaryestablished 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 coursecourse, 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. First line opens window titled "test", on which we can draw. On my computer, it's about 300x300 pixels (more about pixels later) (you can try to type for graphics_nsb_nf instead and get even simpler window) Second install trapclose handler – names a label which will get called on closing window. Third prevent our program from closing immediately :) And two last lines are called then window is closed by Alt-F4 or by mouse – they explicitly close window and end program. (Without that, you can end up with no visible windows BUT Basic still running in memory). Two more things. IfThe 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 anymore,in view you type nomainwin oncan suppress it by typing "nomainwin" at the top of your program. And if 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. That is it, exactly?Ahhh pixels, exactly! They are just dots on your monitor. Anything you see on a monitor is composed fromof colored dots, named pixels. TheA pixel is the smallest element of aany picture. Your At this moment your monitor just now hasis showing pixels in it's current resolution, likesay 1280x1024. It'sThat is the size of the screen in pixels. ("just now" 'cause it couldpixels, (it can be changed). And it's maximalchanged ) and is the maximum size of athe window you can show (andand draw upon).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]] (On In math they teach another coordinate plane, there axes go from left to right and from bottom to top. And where are plenty placeof space is envisioned between whole points. [[image:graph_tutorial_p2.gif]] We are going to respect that then plotting functions).functions. ==Make==Set a dot==Pixel== So. We know that a screen consist of dots,pixels, and dotspixels have coordinates. So all we should actually do to draw a dotpixel – 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). We gotSo we set a single pixel to black dot in theat 100, 100 coordinates (100coordinates. That is 100 pixels to the right and down from top-left corner of window. More(More exactly, from top-left corner of window client area – it's "working zone"). ==Make ==**Set a bunch of dots== ButPixels**== A single point is barely visible. Let's make a bunch, shall we? But how we 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 todo we put these numbers inside "set" command? Let's see. Actually,see, literal line would look like this, > {{#gr "set 100 100"}} is a shortcut. Handy, but shortcut. Full version look so: > {{print #gr, "set 100 100"}} Wait! It looks familiar? It turns out that our graphic command (as ALL graphic commands) is an ordinary PRINT operator. We print to a window (#gr is a window handle) string, containing command. That answers question how put numbers in command. Just like in PRINT: > {{#gr "set ";100; " ";100}} Or> if we use variables we must preserve spaces, > {{X=100: Y=100}} > {{#gr "set ";X; " ";Y}} Just watch your spaces. (There is handy consequence.(Handy tip: If your drawing command does not work as expected, turn them in to ordinary PRINT statements (by replacing {{print}} for {{#gr}} )#gr with print) and examine itthe line in the mainwin.) Clearing that,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 drawdrew a line. I'm pretty sure there must be more straightforward wayways to draw a straight line?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 You can also place a first point inwith a dedicated command and then issue command to draw from lastthis 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"}} ) Same thing about putting if we use variables we must preserve spaces in commands apply.command lines. (It applies EVERYWHERE). ==Why, it's a circle! – circle== Damn, our first bunch of points behaved too simple.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? We gotA 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]]