318c354cbf21b8b8159a5e2f3a966145ce736de0
[sdk] / samples / guiAndGfx / openrider / form1.ec
1 import "ecere"
2 import "game"
3
4 #define PREFIX "OpenRider Track\n\n"
5 #define PREFIX_SIZE 17
6
7 define app = ((GuiApplication)__thisModule);
8
9 #define ROTATION_RESOLUTION 128
10
11 struct Matrix2D {
12    double t0,t1;
13    double t2,t3;
14
15    //sets the angle of the matrix's forward transformation to the angle specified
16    void ToAngle(double radians) {
17       t0 = cos(radians);
18       t1 = -sin(radians);
19       t2 = sin(radians);
20       t3 = cos(radians);
21    }
22    void Scale(double multiplier) {
23       t0 *= multiplier;
24       t1 *= multiplier;
25       t2 *= multiplier;
26       t3 *= multiplier;
27    }
28 };
29
30 //This function takes src, transforms it at origin sx, sy using the _inverse_ of trans_inv, and places it in dest at dx, dy
31 void Transform888Bitmap(Bitmap src, Bitmap dest, double sx, double sy, double dx, double dy, Matrix2D trans_inv, ColorAlpha background) {
32    int destx,desty;
33    int destw=dest.width, desth=dest.height;
34    int srcx,srcy;
35    int srcw=src.width, srch=src.height;
36    uint tt0 = (uint)(trans_inv.t0*65536);
37    uint tt1 = (uint)(trans_inv.t1*65536);
38    uint tt2 = (uint)(trans_inv.t2*65536);
39    uint tt3 = (uint)(trans_inv.t3*65536);
40    uint dsrcx, dsrcy;
41    uint isx = (uint)(sx*65536);
42    uint isy = (uint)(sy*65536);
43    uint t0m, t1m, t2m, t3m, t0mi, t2mi;
44    ColorAlpha *rp = (ColorAlpha*)dest.picture;
45    ColorAlpha *p;
46    ColorAlpha *sp = (ColorAlpha*)src.picture;
47    ColorAlpha *s;
48    t0mi = (uint)(-dx*trans_inv.t0*65536);
49    t2mi = (uint)(-dx*trans_inv.t2*65536);
50    t1m  = (uint)(-dy*trans_inv.t1*65536);
51    t3m  = (uint)(-dy*trans_inv.t3*65536);
52    for (desty=0; desty<desth; desty++, t1m+=tt1, t3m+=tt3, rp+=dest.stride) {
53       p = rp;
54       t0m = t0mi;
55       t2m = t2mi;
56       for (destx=0; destx<destw; destx++, t0m+=tt0, t2m+=tt2,p++) {
57          uint aax, aay;
58          ColorAlpha c[4];
59          uint dma, dmb, dmc, dmd;
60          unsigned short ma,mb,mc,md;
61          dsrcx = t0m + t1m + isx;
62          dsrcy = t2m + t3m + isy;
63          srcx = dsrcx>>16;
64          srcy = dsrcy>>16;
65          aax = dsrcx & 0xFFFFL;
66          aay = dsrcy & 0xFFFFL;
67          dmd = (aax*aay)>>16;
68          dma = (1UL<<16)-aax-aay+dmd;
69          dmb = aax-dmd;
70          dmc = aay-dmd;
71          ma = (unsigned short)(dma>>8);
72          mb = (unsigned short)(dmb>>8);
73          mc = (unsigned short)(dmc>>8);
74          md = (unsigned short)(dmd>>8);
75          s = sp + srcy*src.stride + srcx;
76
77          if (srcx>=0 && srcx<srcw-1 && srcy>=0 && srcy<srch-1) {
78             c[0] = s[0];
79             c[1] = s[1];
80             c[2] = s[src.stride];
81             c[3] = s[src.stride+1];
82          } else {
83             c[0] = background;
84             c[1] = background;
85             c[2] = background;
86             c[3] = background;
87             if (srcx>=0 && srcx<srcw && srcy>=0 && srcy<srch)
88                c[0] = s[0];
89             if (srcx+1>=0 && srcx+1<srcw && srcy>=0 && srcy<srch)
90                c[1] = s[1];
91             if (srcx>=0 && srcx<srcw && srcy+1>=0 && srcy+1<srch)
92                c[2] = s[src.stride];
93             if (srcx+1>=0 && srcx+1<srcw && srcy+1>=0 && srcy+1<srch)
94                c[3] = s[src.stride+1];
95          }
96          p->a = (byte)((c[0].a*ma + c[1].a*mb + c[2].a*mc + c[3].a*md) >> 8);
97          p->color.b = (byte)((c[0].color.b*ma + c[1].color.b*mb + c[2].color.b*mc + c[3].color.b*md) >> 8);
98          p->color.g = (byte)((c[0].color.g*ma + c[1].color.g*mb + c[2].color.g*mc + c[3].color.g*md) >> 8);
99          p->color.r = (byte)((c[0].color.r*ma + c[1].color.r*mb + c[2].color.r*mc + c[3].color.r*md) >> 8);
100       }
101    }
102 }
103
104 int angle2index(double angle) {
105    int index;
106    angle += Pi/ROTATION_RESOLUTION;
107    index = (int)(angle*ROTATION_RESOLUTION/(2*Pi));
108    index %= ROTATION_RESOLUTION;
109    if (index<0)
110       index += ROTATION_RESOLUTION;
111    return index;
112 }
113
114 /*wheel[i] = {alphaBlend=true};
115          wheel[i].Allocate(null, renderwidth, renderwidth, 0, pixelFormat888, false);
116          mat.ToAngle(angle);
117          mat.Scale(64.0/(double)renderwidth);
118          Transform888Bitmap(orig, wheel[i], 31.5, 31.5, ((double)renderwidth-0.5)/2, ((double)renderwidth-0.5)/2, mat, wheel_background);
119          angle += angle_change;*/
120
121 Bitmap CreateRotatedBitmap(Bitmap original, double scale, double angle, ColorAlpha background) {
122    uint renderwidth = (uint)((double)original.width*scale);
123    Bitmap ret {alphaBlend = true};
124    Matrix2D mat;
125    ret.Allocate(null, renderwidth, renderwidth, 0, pixelFormat888, false);
126    mat.ToAngle(-angle);
127    mat.Scale(1/scale);
128    Transform888Bitmap(original, ret, (double)original.width/2-0.5, (double)original.height/2-0.5, ((double)renderwidth-0.5)/2, ((double)renderwidth-0.5)/2, mat, background);
129    return ret;
130 }
131
132 enum DrawingTool {browse, pencil, line, eraser};
133
134 struct Camera2D {
135    double x,y;
136    double zoom; //the higher the zoom, the larger things appear
137 };
138
139 static FileFilter fileFilters[] =
140 {
141    { "OpenRider track files (*.ort)", "ort" },
142    { "All files", null},
143 };
144
145 static FileType fileTypes[] =
146 {
147    { "OpenRider track files", "ort", always }
148 };
149
150 class Form1 : Window
151 {
152    text = "OpenRider";
153    background = activeBorder;
154    borderStyle = sizable;
155    hasMaximize = true;
156    hasMinimize = true;
157    hasClose = true;
158    hasMenuBar = true;
159    size = { 628, 500 };
160    anchor = { horz = -33, vert = -28 };
161
162    bool drawing;
163    double lastx, lasty;
164    double curx, cury;
165    bool navigating;
166    double cstartx, cstarty;
167    double nx, ny;
168    DrawingTool drawingTool; //all tools operated with the left mouse button
169    GameLineType gameLineType;
170    Camera2D camera[2];
171    bool running_b;
172    double lastFrameTime;
173    running_b = false;
174    property bool running {
175       set {
176          if (!running_b && value)
177             lastFrameTime = GetTime();
178          running_b = value;
179          goItem.checked = value;
180          wheelSpinner.started = value;
181          
182          centerItem.disabled = value;
183          nudgeLeftItem.disabled = value;
184          nudgeRightItem.disabled = value;
185          nudgeUpItem.disabled = value;
186          nudgeDownItem.disabled = value;
187          
188          if (!value)
189             Update(null);
190       }
191       get {
192          return running_b;
193       }
194    }
195
196    Timer wheelSpinner {this, 0.015, started=false;
197       bool DelayExpired() {
198          double t = GetTime();
199          
200          this.modifiedDocument = true;
201          
202          //game.FrameMulti(LOGICAL_FPS/DRAWN_FPS*2);
203          game.FrameMulti((uint)((t-lastFrameTime)*LOGICAL_FPS));
204          lastFrameTime = t;
205          
206          //let's center the camera on the ball
207          {
208             Coord2D p = game.vehicles[0].location;
209             Vector2D v = game.vehicles[0].velocity;
210             //camera.zoom = 1.0;
211             camera[1].x = p.x - v.x*5;
212             camera[1].y = p.y - v.y*5;
213          }
214          Update(null);
215          return true;
216       }
217    };
218
219    Bitmap wheel[ROTATION_RESOLUTION];
220    Bitmap wheel_orig {alphaBlend=true};
221
222    ColorAlpha wheel_background;
223    Surface surface;
224    drawing = false;
225    navigating = false;
226    drawingTool = pencil;
227    gameLineType = regular;
228    FileDialog fileSaveDialog
229    {
230       master = this, type = save, text = "Save Track...",
231       types = fileTypes, sizeTypes = sizeof(fileTypes),
232       filters = fileFilters, sizeFilters = sizeof(fileFilters);
233    };
234    FileDialog openDialog
235    {
236       master = this, type = open, text = "Load Track...",
237       types = fileTypes, sizeTypes = sizeof(fileTypes),
238       filters = fileFilters, sizeFilters = sizeof(fileFilters);
239    };
240    saveDialog = fileSaveDialog;
241    Menu fileMenu {menu, "File", f};
242    MenuItem fileNewItem {
243          fileMenu, "New", n, Key {n, ctrl = true};
244
245          bool NotifySelect(MenuItem selection, Modifiers mods)
246          {
247             if (MessageBox {master=this, text = "OpenRider", 
248                contents="Are you sure you want to clear the current track and start a new one?",
249                type=yesNo}.Modal() != yes)
250                return true;
251             running = false;
252             camera[0] = camera[1] = {0.0,0.0,1.0};
253             game.Clear();
254             game.ResetBall();
255             this.modifiedDocument = false;
256             this.fileName = null;
257             Update(null);
258             return true;
259          }
260       }
261    MenuItem fileOpenItem {
262          fileMenu, "Open...", o, Key {o, ctrl = true};
263
264          bool NotifySelect(MenuItem selection, Modifiers mods)
265          {
266             if (openDialog.Modal() == ok) {
267                OnLoadFile(openDialog.filePath);
268                /*if (!OnLoadFile(openDialog.filePath))
269                {
270                   MessageBox {master=this, text = "Error opening file.", contents = "Error opening file."}.Modal();
271                   return true;
272                }*/
273             }
274             return true;
275          }
276       }
277    MenuItem fileSaveItem { fileMenu, "Save", s, Key {s, ctrl = true}, NotifySelect=MenuFileSave}
278    MenuItem fileSaveAsItem { fileMenu, "Save As...", a, Key {a, ctrl = true}, NotifySelect=MenuFileSaveAs }
279    MenuDivider {fileMenu};
280    MenuItem exitItem {fileMenu, "Exit", x, altF4, NotifySelect = MenuFileExit };
281    
282    Menu viewMenu {menu, "View", v};
283    MenuItem centerItem {viewMenu, "Center on ball", c, tab;
284       bool NotifySelect(MenuItem selection, Modifiers mods) {
285          if (!running) {
286             camera[0].x = game.vehicles[0].location.x;
287             camera[0].y = game.vehicles[0].location.y;
288             Update(null);
289          }
290          return true;
291       }
292    }
293    MenuDivider {viewMenu};
294    MenuItem zoomInItem {viewMenu, "Zoom in\t+", i;
295       bool NotifySelect(MenuItem selection, Modifiers mods) {
296          Zoom(1.5, 0);
297          return true;
298       }
299    }
300    MenuItem zoomOutItem {viewMenu, "Zoom out\t-", o;
301       bool NotifySelect(MenuItem selection, Modifiers mods) {
302          Zoom(1/1.5, 0);
303          return true;
304       }
305    }
306    MenuDivider {viewMenu};
307    MenuItem nudgeLeftItem {viewMenu, "Left", l, left, NotifySelect = NudgeCallback, id=0}
308    MenuItem nudgeRightItem {viewMenu, "Right", r, right, NotifySelect = NudgeCallback, id=1}
309    MenuItem nudgeUpItem {viewMenu, "Up", u, up, NotifySelect = NudgeCallback, id=2}
310    MenuItem nudgeDownItem {viewMenu, "Down", d, down, NotifySelect = NudgeCallback, id=3}
311    
312    bool NudgeCallback(MenuItem selection, Modifiers mods) {
313       Camera2D *c = camera;
314       double nudgeAmount = 20.0 / c->zoom;
315       
316       if (running)
317          return true;
318       
319       if (selection.id == 0)
320          c->x -= nudgeAmount;
321       else if (selection.id == 1)
322          c->x += nudgeAmount;
323       else if (selection.id == 2)
324          c->y -= nudgeAmount;
325       else
326          c->y += nudgeAmount;
327       
328       Update(null);
329       
330       return true;
331    }
332    
333    Menu toolMenu {menu, "Tool", t};
334    MenuItem browseItem {toolMenu, "Browse", b, b, checked=false, isRadio=true, NotifySelect = MyCustomMenu};
335    MenuItem pencilItem {toolMenu, "Draw", d, d, checked=true, isRadio=true, NotifySelect = MyCustomMenu};
336    MenuItem lineItem {toolMenu, "Straight line", s, s, checked=false, isRadio=true, NotifySelect = MyCustomMenu};
337    MenuItem eraserItem {toolMenu, "Eraser", e, e, checked=false, isRadio=true, NotifySelect = MyCustomMenu};
338    MenuDivider {toolMenu};
339    MenuItem normalItem {toolMenu, "Normal", a, a, checked=true, isRadio=true, NotifySelect = MyCustomMenu};
340    MenuItem speedItem {toolMenu, "Right accelerator", r, r, checked=false, isRadio=true, NotifySelect = MyCustomMenu};
341    MenuItem slowItem {toolMenu, "Left accelerator", f, f, checked=false, isRadio=true, NotifySelect = MyCustomMenu};
342    MenuItem decorItem {toolMenu, "Decoration", c, c, checked=false, isRadio=true, NotifySelect = MyCustomMenu};
343    
344    Menu controlMenu {menu, "Control", c};
345    MenuItem goItem {controlMenu, "Go", g, Key {g, ctrl=true}, checked=false, checkable=true;
346       bool NotifySelect(MenuItem selection, Modifiers mods)
347       {
348          running = selection.checked;
349          return true;
350       }
351    }
352    MenuItem resetItem {controlMenu, "Reset Ball", r, Key {r, ctrl=true};
353       bool NotifySelect(MenuItem selection, Modifiers mods)
354       {
355          this.modifiedDocument = true;
356          running = false;
357          game.ResetBall();
358          Update(null);
359          return true;
360       }
361    }
362
363    bool MyCustomMenu(MenuItem selection, Modifiers mods) {
364       if (browseItem.checked)
365          drawingTool = browse;
366       else if (pencilItem.checked)
367          drawingTool = pencil;
368       else if (lineItem.checked)
369          drawingTool = line;
370       else if (eraserItem.checked)
371          drawingTool = eraser;
372       
373       if (normalItem.checked)
374          gameLineType = regular;
375       else if (speedItem.checked)
376          gameLineType = speed;
377       else if (slowItem.checked)
378          gameLineType = slow;
379       else if (decorItem.checked)
380          gameLineType = decor;
381       return true;
382    }
383
384    bool OnCreate(void)
385    {
386       camera[0] = {0.0, 0.0, 1.0}; //paused camera
387       camera[1] = {0.0, 0.0, 1.0}; //running camera
388       app.timerResolution = 300;
389       game.Init();
390       return true;
391    }
392
393    bool OnSaveFile(char * fileName)
394    {
395       File f = FileOpen(fileName, write);
396       unsigned short mv = (unsigned short)game.version_code;
397       char vn[16];
398       if (!f)
399          return false;
400       memset(vn, 0, 16);
401       strcpy(vn, version_name);
402       f.Write(PREFIX, 1, PREFIX_SIZE);
403       f.Write(vn, 1, 16);
404       f.Put(mv);
405       f.Put(camera[0]);
406       f.Put(game);
407       delete f;
408       this.modifiedDocument = false;
409       return true;
410    }
411
412    void OnLoadFile(char * fileName) {
413       File f = FileOpen(fileName, read);
414       unsigned short mv;
415       char prefix[PREFIX_SIZE];
416       char vn[16];
417
418       if (!f) {
419          MessageBox {master=this, text="OpenRider", contents="Error loading file."}.Modal();
420          return;
421       }
422
423       f.Read(prefix, 1, sizeof(prefix));
424       if (strncmp(prefix, PREFIX, sizeof(prefix))) {
425          MessageBox {master=this, text="OpenRider", contents="This does not appear to be a valid OpenRider track.  Try using an older or newer version of OpenRider."}.Modal();
426          delete f;
427          return;
428       }
429       f.Read(vn, 1, 16);
430       vn[15] = 0;
431       f.Get(mv);
432       
433       if (mv > max_version_code_supported) {
434          const char *vnp = *vn ? vn : "www.thedailywtf.com";
435          const char *message_format = "This track was created by a newer version of OpenRider (version %s).  Continue opening it?";
436          char *s = new char[strlen(message_format)+strlen(vnp)];
437          DialogResult result;
438          sprintf(s, message_format, vnp);
439          result = MessageBox {master=this, text="OpenRider", type=yesNo, contents=s}.Modal();
440          delete s;
441          if (result==no) {
442             delete f;
443             return;
444          }
445       }
446       f.Get(camera[0]);
447       f.Get(game);
448       running = false;
449       camera[1] = {0,0,1.0};
450       this.fileName = openDialog.filePath;
451       this.modifiedDocument = false;
452       Update(null);
453       delete f;
454       return;
455    }
456
457    bool OnLoadGraphics() {
458       uint i;
459
460       wheel_orig.Load(":wheel.png", "png", null);
461       if (!wheel_orig) {
462          MessageBox {master=this, contents="Wheel image could not be loaded."}.Modal();
463          return false;
464       }
465       #if 1
466       wheel_orig.Convert(null, pixelFormat888, null);
467       if (wheel_orig.pixelFormat != pixelFormat888) {
468          MessageBox {master=this, contents="Wheel image could not be converted."}.Modal();
469          return false;
470       }
471
472       //let's assume the topleft pixel of the wheel is background
473       wheel_background = *(ColorAlpha*)wheel_orig.picture;
474
475       for (i=0; i<ROTATION_RESOLUTION; i++)
476          wheel[i] = CreateRotatedBitmap(wheel_orig, 48.0/64.0, (double)i*2*Pi/ROTATION_RESOLUTION, wheel_background);
477       
478       for (i=0; i<ROTATION_RESOLUTION; i++)
479          wheel[i].MakeDD(displaySystem);
480       #else
481       wheel_orig.MakeDD(displaySystem);
482       for (i=0; i<ROTATION_RESOLUTION; i++)
483          wheel[i] = wheel_orig;
484       #endif
485       
486       return true;
487    }
488
489    void OnUnloadGraphics() {
490       uint i;
491       for (i=0; i<ROTATION_RESOLUTION; i++)
492          delete wheel[i];
493    }
494
495    void OnRedraw(Surface surface)
496    {
497       Camera2D *c = &camera[running];
498       double x0 = (double)(-clientSize.w) / c->zoom + c->x;
499       double y0 = (double)(-clientSize.h) / c->zoom + c->y;
500       double x1 = (double)(clientSize.w) / c->zoom + c->x;
501       double y1 = (double)(clientSize.h) / c->zoom + c->y;
502       this.surface = surface;
503       //cameraLabel.SetText("%f, %f, %f", c->x, c->y, c->zoom);
504       if (drawing && drawingTool==line)
505          game.DrawLine(this, {lastx, lasty, curx, cury, gameLineType, floor});
506       game.DrawFrame(x0,y0,x1,y1);
507    }
508
509    bool OnLeftButtonDown(int x, int y, Modifiers mods)
510    {
511       Camera2D *c = &camera[running];
512       if (drawingTool==browse) {
513          if (running)
514             return true;
515          cstartx = c->x;
516          cstarty = c->y;
517          nx = (double)x;
518          ny = (double)y;
519       } else
520          this.modifiedDocument = true;
521       drawing = true;
522       lastx = (double)(x-(clientSize.w>>1))/c->zoom+c->x;
523       lasty = (double)(y-(clientSize.h>>1))/c->zoom+c->y;
524       Capture();
525       dnTimer.Start();
526       return true;
527    }
528
529    bool OnLeftButtonUp(int x, int y, Modifiers mods)
530    {
531       Camera2D *c = &camera[running];
532       if (drawing && drawingTool==line) {
533          curx = (double)(x-(clientSize.w>>1)) / c->zoom + c->x;
534          cury = (double)(y-(clientSize.h>>1)) / c->zoom + c->y;
535          AddLineToGame(lastx, lasty, curx, cury);
536          Update(null);
537       }
538       drawing = false;
539       if (!navigating) {
540          ReleaseCapture();
541          dnTimer.Stop();
542       }
543       return true;
544    }
545
546    bool OnMiddleButtonDown(int x, int y, Modifiers mods)
547    {
548       Camera2D *c = &camera[running];
549       if (running)
550          return true;
551       cstartx = c->x;
552       cstarty = c->y;
553       nx = (double)x;
554       ny = (double)y;
555       navigating = true;
556       dnTimer.Start();
557       Capture();
558       return true;
559    }
560
561    bool OnMiddleButtonUp(int x, int y, Modifiers mods)
562    {
563       navigating = false;
564       if (!drawing) {
565          ReleaseCapture();
566          dnTimer.Stop();
567       }
568       return true;
569    }
570
571    bool OnKeyHit(Key key, unichar ch)
572    {
573       if (key.modifiers)
574          return true;
575       switch((SmartKey)key)
576       {
577          case space:
578             running = !running;
579             break;
580          case tab:
581             if (running) {
582                //If the game is running, set the pauseCamera to the current camera
583                camera[0] = camera[1];
584             } else {
585                //Otherwise, center the camera on the ball
586                camera[0].x = game.vehicles[0].location.x;
587                camera[0].y = game.vehicles[0].location.y;
588                Update(null);
589             break;
590          case wheelDown:
591             Zoom(1/1.05, true);
592             break;
593          case wheelUp:
594             Zoom(1.05, true); 
595             break;
596          case minus:
597             Zoom(1/1.25, false);
598             break;
599          case plus:
600             Zoom(1.25, false);
601             break;
602          }
603       }
604       return true;
605    }
606    Timer dnTimer {this, 0.03, false;
607       bool DelayExpired()
608       {
609          Camera2D *c = &camera[running];
610          bool needUpdate = false;
611          int x,y;
612          double fx, fy;
613          
614          GetMousePosition(&x, &y);
615          fx = (double)(x-(clientSize.w>>1))/c->zoom+c->x;
616          fy = (double)(y-(clientSize.h>>1))/c->zoom+c->y;
617          if (fx==curx && fy==cury)
618             return true;
619          curx = fx;
620          cury = fy;
621          if (drawing && drawingTool!=browse) {
622             this.modifiedDocument = true;
623             if (drawingTool==pencil) {
624                AddLineToGame(lastx, lasty, curx, cury);
625                lastx = curx;
626                lasty = cury;
627                needUpdate = true;
628             } else if (drawingTool==line) {
629                needUpdate = true;
630                //game.DrawLine(this, {lastx, lasty, curx, cury, regular, floor});
631             } else if (drawingTool==eraser) {
632                double screenEraserSize = 8.0;
633                double eraserSize = screenEraserSize/c->zoom;
634                if (game.EraseAtBox(curx-eraserSize, cury-eraserSize, curx+eraserSize, cury+eraserSize))
635                   needUpdate = true;
636             }
637          }
638          
639          if (navigating || (drawing && drawingTool==browse)) {
640             double ox = nx-(double)x;
641             double oy = ny-(double)y;
642             ox /= c->zoom;
643             oy /= c->zoom;
644             c->x = cstartx + ox;
645             c->y = cstarty + oy;
646             needUpdate = true;
647          }
648          
649          if (needUpdate)
650             Update(null);
651          return true;
652       }
653    };
654    Label cameraLabel { this, size = { 228, 13 }, position = { 384, 24 } };
655    Game game {this;
656       void DrawBall(GameBall ball) {
657          Camera2D *c = &camera[running];
658          int x= (int)((ball.location.x-c->x)*c->zoom)+(clientSize.w>>1);
659         int y= (int)((ball.location.y-c->y)*c->zoom)+(clientSize.h>>1);
660          int rwidth = (int)(ball.radius*2*c->zoom);
661          Bitmap w = wheel[angle2index(ball.angle)];
662          
663          if (w) {
664             x -= rwidth>>1;
665             y -= rwidth>>1;
666             if (rwidth == w.width)
667                surface.Blit(w, x,y, 0,0, w.width,w.height);
668             else
669                surface.Filter(w, x,y, 0,0, rwidth,rwidth, w.width,w.height);
670          }
671          
672          #if 0
673          if (!running && fabs(camera.zoom-1.0)>0.01) {
674             double newradius = ball.radius*camera.zoom;
675             if (fabs(newradius-pause_wheel_radius)>0.01) {
676                delete pause_wheel;
677                pause_wheel = CreateRotatedBitmap(wheel_orig, newradius*2/(double)wheel_orig.width, ball.angle, wheel_background);
678                if (pause_wheel) {
679                   pause_wheel.MakeDD(displaySystem);
680                   pause_wheel_radius = newradius;
681                }
682             }
683             if (pause_wheel)
684                w = pause_wheel;
685          }
686          if (w) {
687             x -= w.width>>1;
688             y -= w.height>>1;
689             //surface.Stretch(w, x-(rwidth>>1),y-(rwidth>>1), 0,0,rwidth,rwidth,w.width,w.height);
690             //This will crash:  surface.Blit(CreateRotatedBitmap(wheel_orig, 21.7331610967957,0,wheel_background), 2147483263, 2147483176,0,0,1390,1390);
691             //We will only draw the bitmap if it is visible on the screen.  Otherwise, we might get a crash.
692             if (x>-w.width && x<clientSize.w && y>-w.height && y<clientSize.h) {
693                surface.Blit(w, x,y,0,0,w.width,w.height);
694                surface.Filter(w, x-(rwidth>>1),y-(rwidth>>1), 0,0,rwidth,rwidth,w.width,w.height);
695                //surface.Filter(w, x,y,0,0,w.width*2,w.height*2,w.width,w.height);
696             }
697          }
698          #endif
699       }
700
701       void DrawLine(GameLine line) {
702          Camera2D *c = &camera[running];
703          int x0=(int)((line.x0-c->x)*c->zoom)+(clientSize.w>>1);
704         int y0=(int)((line.y0-c->y)*c->zoom)+(clientSize.h>>1);
705         int x1=(int)((line.x1-c->x)*c->zoom)+(clientSize.w>>1);
706         int y1=(int)((line.y1-c->y)*c->zoom)+(clientSize.h>>1);
707
708          /*if (line.floor_or_ceiling == floor) {
709             surface.SetForeground(gray);
710             DrawThickLine(x0, y0+2, x1, y1+2);
711          } else if (line.floor_or_ceiling == ceiling) {
712             surface.SetForeground(gray);
713             DrawThickLine(x0, y0-2, x1, y1-2);
714          }*/
715
716          switch (line.type) {
717             case regular:
718                surface.SetForeground(black);
719                break;
720             case decor:
721                surface.SetForeground(green);
722                break;
723             case slow:
724                surface.SetForeground(darkGoldenrod);
725                break;
726             case speed:
727                surface.SetForeground(red);
728                break;
729          }
730          
731          DrawThickLine(x0, y0, x1, y1);
732       }
733    };
734
735    void AddLineToGame(double x0, double y0, double x1, double y1) {
736       game.lines.Add({x0,y0,x1,y1,gameLineType,floor});
737    }
738
739    void DrawThickLine(int x0, int y0, int x1, int y1) {
740       surface.DrawLine(x0,y0,x1,y1);
741       surface.DrawLine(x0+1,y0,x1+1,y1);
742       surface.DrawLine(x0,y0+1,x1,y1+1);
743       surface.DrawLine(x0+1,y0+1,x1+1,y1+1);
744    }
745
746    void Zoom(double scale_factor, bool zoomTowardMouse) {
747       Camera2D *c = &camera[running];
748       if (!running) {
749          if (zoomTowardMouse) { //zoom toward mouse
750             int x,y;
751             GetMousePosition(&x, &y);
752             curx = (double)(x-(clientSize.w>>1)) / c->zoom + c->x;
753             cury = (double)(y-(clientSize.h>>1)) / c->zoom + c->y;
754          } else { //zoom toward ball
755             curx = game.vehicles[0].location.x;
756             cury = game.vehicles[0].location.y;
757          }
758          //we want (curx-c->x)*zoom*scale factor to equal (curx-old c->x)*zoom
759          c->x = (curx*scale_factor-curx+c->x)/scale_factor;
760          c->y = (cury*scale_factor-cury+c->y)/scale_factor;
761          c->zoom *= scale_factor;
762          Update(null);
763       } else
764          c->zoom *= scale_factor;
765    }
766 }
767
768 Form1 form1 {};