#pragma rtGlobals=1 // Use modern global access method. Menu "Macros" SubMenu "Draw a dartboard ..." "relative coordinates", /Q, DrawRelativeDartBoard("DBrel") "absolute coordinates", /Q, DrawAbsoluteDartBoard("DBabs") "interactive board", /Q, DrawInteractiveDartBoard("DBint") End End function DrawRelativeDartBoard(DBfolder) string DBfolder NewDartBoard(DBfolder,0.5,0.5,0.5,"rel") // dartboard with relative coordinate system Display /W=(40,40,240,240) // size = 200 x 200 DrawDartBoard(DBfolder) end function DrawAbsoluteDartBoard(DBfolder) string DBfolder NewDartBoard(DBfolder,100,100,100,"abs") // dartboard with absolute coordinate system Display /W=(40,40,240,240) // same size as above DrawDartBoard(DBfolder) end function DrawInteractiveDartBoard(DBfolder) string DBfolder // 1. draw the board string DBwinname = "InteractiveDartboard" NewDartBoard(DBfolder,.5,.5,.5,"rel") // dartboard with relative coordinate system DoWindow/K $DBwinname // make sure there is no older interactive dartboard Display /N=$DBwinname /W=(40,40,400,400) // size = 360 x 360 ModifyGraph height={Aspect,1} // keep the graph square so circles are round DrawDartBoard(DBfolder) // 2. set up userdata and hook function // note: Rather than using the window name and some string fiddling, a dartboard's hook function // determines the dartboard's data folder name from this userdata. Although the hook function // is tied to the window anyway, this mechanism allow us to choose the window name // independently of the dartboard's data folder name, which is determined at call-time. SetWindow $DBwinname, userdata(DBfolder)= DBfolder SetWindow $DBwinname, hook(DartBoardClickHook)= InteractiveDartBoardHook, hookEvents= 1 // 3. add a basic read-out box for the score string DBfolderPath = "root:Packages:Dartboards:"+DBfolder TextBox/N=text0/A=LT/X=0.5/Y=0.5 "\\{"+DBfolderPath+":DBhitScore}" // AppendText/N=text0/NOCR " (\\{"+DBfolderPath+":DBhitNumber},\\{"+DBfolderPath+":DBhitRing})" // DEBUG end // create dartboard data folder function NewDartBoard(DBfolder, xOrg, yOrg, radius, coords) string DBfolder variable xOrg, yOrg, radius string coords string saveDF = GetDataFolder(1) NewDataFolder/O/S root:Packages NewDataFolder/O/S Dartboards // 1. create global data for all dartboards // note: This data is re-created every time a new dartboard is created -- not very efficient. // This would better be implemented in an "initpackage" call, but here we avoid this overhead for the casual user. // 1.1 create sound wave to be played in hook function Make/W/O/N=(2000,2) DBsoundwave // 16 bit data (stereo sound) SetScale/P x, 0, 1e-4, DBsoundwave // set sample rate to 10 kHz DBsoundwave= 2000*sin(2*pi*x*(666+(0.5-q)*999*x)) // rising+falling pitch = mysterious sound // listen to this by saying: PlaySound/A DBsoundwave // 1.2 create global data that is useful for any dartboard Make/B/U/O/N=20 DBnumbers = {6,13,4,18,1,20,5,12,9,14,11,8,16,7,19,3,17,2,15,10} // the numbers Make/O/N=(2,5,3) DBcolors = {{{0,5,0},{5,5,5},{0,5,0},{5,5,5},{1,1,1}}, {{5,0,0},{1,1,1},{5,0,0},{1,1,1},{5,5,5}}} DBcolors *= 10000 // the colors (from edge to center) = {green, grey, green, grey, black} // or {red, black, red, black, grey}, the last entry is for the text Make/D/O/N=8 DBradii = {1.00,0.95,0.63,0.58,0.094,0.037,0.0,0.78} // fractional radii, last: text // 2. data specific for this dartboard NewDataFolder/O/S $(DBfolder) Duplicate/O ::DBradii DBradii // copy fractional radii for the generic dartboard DBradii *= radius // and scale to given radius Variable/G DBradius = radius // actual radius Variable/G DBxOrigin = xOrg // x origin Variable/G DByOrigin = yOrg // y origin String/G DBcoordSys = coords // coordinate system ("abs" or "rel") // note: the following three variables are important only for interactive dartboards, but we create them always Variable/G DBhitRing = 0 // hit ring (0..6 = outside, double, simple, treble, simple, outer bull, inner bull) Variable/G DBhitNumber = 0 // hit number (0..20, where 0 means "bull") Variable/G DBhitScore = 0 // score associated with the combination of the prior two data SetDataFolder saveDF end // DrawDartBoard draws a dart board... // ... originally programmed to test DrawArcWedge with different coordinate systems. // The coordinate system is actually chosen outside this function (see macros at top of this proc file) function DrawDartBoard(DBfolder) string DBfolder // 1. retrieve information about the dart board if( !DataFolderExists("root:Packages:Dartboards:"+DBfolder) ) // sanity check Abort "Dartboard \""+DBfolder+"\" does not exist. Must be initialized first." endif string saveDF = GetDataFolder(1) SetDataFolder root:Packages:Dartboards WAVE num = DBnumbers, col = DBcolors // access global data (numbers and colors) SetDataFolder $(DBfolder) WAVE rad = DBradii // access local data (size/position info) NVAR x0 = DBxOrigin, y0 = DByOrigin, radius = DBradius SVAR coords = DBcoordSys SetDataFolder saveDF // 2. prepare drawing environment SetDrawLayer ProgBack // draw into ProgBack layer to enable "flashing" of hit areas (see hook function) SetDrawEnv xcoord= $coords, ycoord= $coords // set coordinate system to "rel" or "abs" SetDrawEnv textxjust= 1, textyjust= 1, save // this makes placing the text very easy // 3. draw our stuff // 3.1 draw the "ArcWedges" variable m, k, odd for( m = 0; m < 20; m += 1 ) // for each "number" odd = (m/2 != floor(m/2)) // alternating wedges have different colors, odd "numbers" have different color schemes for( k = 0; k < 4; k += 1 ) // wedge for a number consists of 4 "ArcWedges" SetDrawEnv fillfgc=(col[0][k][odd],col[1][k][odd],col[2][k][odd]) // select color DrawArcWedge(x0, y0, rad[k], rad[k+1], 18*m-9, 18*m+9) // draw "ArcWedge" endfor SetDrawEnv textrgb=(col[0][4][odd],col[1][4][odd],col[2][4][odd]) // select color DrawText x0+rad[7]*cos(pi/10*m), y0-rad[7]*sin(pi/10*m), num2str(num[m]) // draw text endfor // 3.2 draw outer and inner bull for( k = 0; k <= 1; k += 1 ) // we need two circles (bull and bull's eye) SetDrawEnv fillfgc=(col[0][0][k],col[1][0][k],col[2][0][k]) // select color DrawOval x0-rad[k+4], y0-rad[k+4], x0+rad[k+4], y0+rad[k+4] // draw circle // DrawArcWedge(x0, y0, rad[k+4], rad[k+5], 0, 360) // this would draw unwanted lines! endfor end // DrawArcWedge draws an arced wedge, i.e. a polygon delimited by two concentric arcs. // Origin = (x0, y0), Arc1 = (r1, phi1-->phi2), Arc2 = (r2, phi2-->phi1), the arcs are connected by lines. // If one of the radii is 0, you get a pizza wedge. If r1==r2, it's just an arc. If r1*r2 < 0, it's a fan. // The coordinate system is similar to DrawArc: 0° is at 3 o'clock, the angle is counterclockwise. // No choice is made for the drawing coordinate system, you have to set it up with SetDrawEnv. // To try out DrawArcWedge without the overhead of the other functions in this proc file, use: // Display; ModifyGraph width={Aspect,1} // make a square graph so that a circle is a circle // SetDrawEnv fillfgc= (47872,47872,47872); DrawArcWedge(0.5, 0.5, 0.25, 0.45, 150, 210) // a grey "c" // SetDrawEnv fillfgc= (65280,43520,0); DrawArcWedge(0.5, 0.5, -0.25, 0.45, 60, 120) // an orange fan // (the easy way: select the three lines above, choose "Decommentize" (Edit menu), then hit "Ctrl+Enter") function DrawArcWedge(x0, y0, r1, r2, phi1, phi2) variable x0, y0, r1, r2, phi1, phi2 // 1. set up basic variables ("hard-wired" section) variable rad = 180 / pi // sin() and cos() need radians, not degrees variable dphi = phi2 - phi1 // difference angle, might be zero variable intphi = 5 // use approx 5° intervals between polygon points in arcs variable nphi = max( floor(dphi / intphi), 1 ) // number of arc points - 1 // "max(.., 1)" prevents division by zero (short arcs also need two end points) dphi /= nphi // (interval for the arc points) = (difference angle) / (number of arc points - 1) // 2. draw the ArcWedge // 2.1 draw the first point on radius r1, at angle phi1 variable angle = phi1 / rad variable x1 = r1*cos(angle), y1 = -r1*sin(angle) // note: this implements the coordinate system of DrawArc DrawPoly x0+x1, y0+y1, 1, 1, {x1, y1} // use an unnamed polygon instead of a wave pair to make each ArcWedge unique // 2.2 draw the arc on radius r2, from angle phi1 to phi2 (implicitely creates line from r1 to r2, at phi1) variable k, kmax kmax = nphi * ( r2 != 0 ) // 1st arc at r2 (only one point iff r2 == 0) for( k = 0; k <= kmax; k += 1 ) // integers are better for the loop to prevent rounding errors angle = (phi1 + k*dphi) / rad DrawPoly/A {r2*cos(angle), -r2*sin(angle)} endfor // 2.3 draw the arc on radius r1, from angle phi2 to phi1 (implicitely creates line from r2 to r1, at phi2) kmax = nphi * ( r1 != 0 ) // 2nd arc at r1 (only one point iff r1 == 0) for( k = 0; k <= kmax; k += 1 ) angle = (phi2 - k*dphi) / rad // note: loop direction inverted DrawPoly/A {r1*cos(angle), -r1*sin(angle)} endfor end // hook function for a interactive dartboard Function InteractiveDartBoardHook(whs) STRUCT WMWinHookStruct &whs variable rval = 0 // return value for Igor, see last line of this function strswitch(whs.eventName) case "mousedown": // 1. get mouse coordinates in useful units // 1.1 determine plot coordinates // works with abs coords so long as window is not resized (iff origin=center) // works with rel coords even if window is resized (iff origin=center) string DBwin = whs.winName string DBfolder = GetUserData(DBwin, "", "DBfolder") string saveDF = GetDataFolder(1) SetDataFolder root:Packages:Dartboards:$(DBfolder) NVAR x00 = DBxOrigin, y00 = DByOrigin, r0 = DBradius // should use these, but don't at the moment NVAR DBhitNumber, DBhitRing, DBhitScore WAVE radii = ::DBradii, nums = ::DBnumbers, sound = ::DBsoundwave GetWindow $(DBwin) wsizeDC // window size in pixels variable dx = V_right-V_left, dy = V_bottom-V_top // extent variable x0 = (V_right+V_left)/2, y0 = (V_bottom+V_top)/2 // center // 1.2 retrieve mouse coordinates and transform... variable mx = (whs.mouseLoc.h-x0)/dx // relative to center (!) variable my = (whs.mouseLoc.v-y0)/dy // relative to center (!) variable radius = 2*sqrt(mx*mx + my*my) // already fractional radius, max = 2 variable angle = atan(-my/mx)*180/pi // invert DrawArc formula // print angle // DEBUG: OK angle += 180*(mx<0) + 360*(mx>0 && my>0) // sign treatment for arctan // print angle // DEBUG: OK // 2. determine which part of the board was hit // (this could also be done outside the hook function by an update procedure) // 2.1 determine ring (singles, doubles, trebles, bull's eye) variable ring for( ring = 0; ring < 6; ring += 1 ) if( radius > radii[ring] ) break endif endfor // 2.2 determine number (1-20 or bull=0) variable awIndex = mod(floor( (angle + 9) / 18 ), 20) variable num = nums[awIndex] if( ring > 4 ) num = 0 // bull or bull's eye endif // 2.3 calculate score variable score = num switch( ring ) case 0: // outside score = 0 break case 1: // doubles score *= 2 break case 3: // trebles score *= 3 break case 5: // bull (outer) score = 25 break case 6: // bull's eye score = 50 endswitch // 2.4 export determined values DBhitRing = ring DBhitNumber = num DBhitScore = score SetDataFolder saveDF // 3. provide visual and auditive feedback // 3.1 flash the hit area SetDrawLayer /W=$DBwin ProgFront // draw in front of dartboard layer (assumed to be ProgBack) // (use ring here instead of DBhitRing to be more compact) variable theta if( 0 < ring && ring < 5 ) // an ArcWedge was hit theta = 18 * awIndex // central angle of ArcWedge DrawArcWedge(x00, y00, r0*radii[ring-1], r0*radii[ring], theta-9, theta+9) else // k == 5 or 6 (bull or bull's eye) -- or k == 0 (outside, flash whole board) DrawOval /W=$DBwin x00-radii[ring-1]/2, y00-radii[ring-1]/2, x00+radii[ring-1]/2, y00+radii[ring-1]/2 endif DoUpdate /W=$DBwin // must update explicitely here Sleep/C=2/T 0 // change cursor to "Click" (C=2), which delays redrawing sufficiently SetDrawLayer /W=$DBwin /K ProgFront // erase the "flashing" layer // 3.2 play a sound when hitting bull's eye or triple-20 if ( ring == 6 || (ring == 3 && num == 20) ) PlaySound/A sound PlaySound/A sound endif rval = 1 // tell Igor that we handled this mouse click endswitch return rval End