#pragma rtGlobals=1 // Use modern global access method. // DrawDartBoard.ipf // version 1.0, 15th February 09 // // Today's suggestions: // 1. Compile this procedure file and try the macros. // 2. Resize the resulting windows, observe the behaviour when using different coordinate systems. // 3. Use the code in your own projects, it's extensively documented. // // Greetings from W. Harneit (Berlin) 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,0.5,0.5,0.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 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 retrieve information about dartboard string DBwin = whs.winName string DBfolder = GetUserData(DBwin, "", "DBfolder") string saveDF = GetDataFolder(1) SetDataFolder root:Packages:Dartboards:$DBfolder // origin, radius, coordinate system NVAR xOrg = DBxOrigin, yOrg = DByOrigin, r0 = DBradius SVAR coords = DBcoordSys // 1.2 recalculate origin and radial (possibly oval) units in pixels variable x0, y0, rx, ry if( !cmpstr(coords,"rel") ) GetWindow $DBwin wsizeDC // window size in pixels variable width = V_right-V_left, height = V_bottom-V_top x0 = xOrg*width y0 = yOrg*height rx = r0*width ry = r0*height else // "abs" coordinates x0 = point2pixel(xOrg) y0 = point2pixel(yOrg) rx = point2pixel(r0) ry = rx endif // 1.3 get mouse coordinates, rescale to natural coordinates variable mx = (whs.mouseLoc.h - x0) / rx // relative to origin, scaled to radial (possibly oval) units variable my = (whs.mouseLoc.v - y0) / ry // relative to origin, scaled to radial (possibly oval) units variable radius = sqrt(mx*mx + my*my) // fractional radius variable angle = atan2(-my, mx) * 180/pi // invert DrawArc formula angle += 360 * (my > 0) // force angle to be between 0 and 360 degrees // print angle // DEBUG: OK // 2. determine which part of the board was hit // retrieve generic information about dartboards WAVE radii = ::DBradii, nums = ::DBnumbers, sound = ::DBsoundwave // 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 NVAR DBhitNumber, DBhitRing, DBhitScore DBhitRing = ring DBhitNumber = num DBhitScore = score SetDataFolder saveDF // 3. provide visual and audio feedback // 3.1 flash the hit area SetDrawLayer /W=$DBwin ProgFront // draw in front of dartboard layer (assumed to be ProgBack) SetDrawEnv xcoord= $coords, ycoord= $coords // set coordinate system to "rel" or "abs" // (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(xOrg, yOrg, 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 xOrg-r0*radii[ring-1], yOrg-r0*radii[ring-1], xOrg+r0*radii[ring-1], yOrg+r0*radii[ring-1] endif // force update and kill some time #if NumberByKey("IGORVERS", IgorInfo(0)) < 6.1 DoUpdate // general update (prior to IP6.1) #else DoUpdate /W=$DBwin // update this graph only (/W flag is new in IP6.1) #endif 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 // 2 functions to handle conversion from/to Igor's Point coordinates (used for drawing in abs mode) // to/from the "local device coordinates" or "pixels" (used for the mouse in window hook functions) function Point2Pixel(p) variable p return p*ScreenResolution/72 end function Pixel2Point(p) variable p return p*72/ScreenResolution end