| OLD | NEW | 
|---|
|  | (Empty) | 
| 1 // Copyright (c) 2011, the Dart project authors.  Please see the AUTHORS file |  | 
| 2 // for details. All rights reserved. Use of this source code is governed by a |  | 
| 3 // BSD-style license that can be found in the LICENSE file. |  | 
| 4 |  | 
| 5 #library('spirodraw'); |  | 
| 6 |  | 
| 7 #import('dart:dom'); |  | 
| 8 #source("ColorPicker.dart"); |  | 
| 9 // TODO(732): Reenable when this works in the VM. |  | 
| 10 // #resource("spirodraw.css"); |  | 
| 11 |  | 
| 12 void main() { |  | 
| 13   new Spirodraw().go(); |  | 
| 14 } |  | 
| 15 |  | 
| 16 class Spirodraw { |  | 
| 17 |  | 
| 18   static double PI2 = Math.PI * 2; |  | 
| 19   Document doc; |  | 
| 20   // Scale factor used to scale wheel radius from 1-10 to pixels |  | 
| 21   int RUnits, rUnits, dUnits; |  | 
| 22   // Fixed radius, wheel radius, pen distance in pixels |  | 
| 23   double R, r, d; |  | 
| 24   HTMLInputElement fixedRadiusSlider, wheelRadiusSlider, |  | 
| 25     penRadiusSlider, penWidthSlider, speedSlider; |  | 
| 26   HTMLSelectElement inOrOut; |  | 
| 27   HTMLLabelElement numTurns; |  | 
| 28   HTMLDivElement mainDiv; |  | 
| 29   num lastX, lastY; |  | 
| 30   int height, width, xc, yc; |  | 
| 31   int maxTurns; |  | 
| 32   HTMLCanvasElement frontCanvas, backCanvas; |  | 
| 33   CanvasRenderingContext2D front, back; |  | 
| 34   HTMLCanvasElement paletteElement; |  | 
| 35   ColorPicker colorPicker; |  | 
| 36   String penColor = "red"; |  | 
| 37   int penWidth; |  | 
| 38   double rad = 0.0; |  | 
| 39   double stepSize; |  | 
| 40   bool animationEnabled = true; |  | 
| 41   int numPoints; |  | 
| 42   double speed; |  | 
| 43   bool run; |  | 
| 44 |  | 
| 45   Spirodraw() { |  | 
| 46     doc = window.document; |  | 
| 47     inOrOut = doc.getElementById("in_out"); |  | 
| 48     fixedRadiusSlider = doc.getElementById("fixed_radius"); |  | 
| 49     wheelRadiusSlider = doc.getElementById("wheel_radius"); |  | 
| 50     penRadiusSlider = doc.getElementById("pen_radius"); |  | 
| 51     penWidthSlider = doc.getElementById("pen_width"); |  | 
| 52     speedSlider = doc.getElementById("speed"); |  | 
| 53     numTurns = doc.getElementById("num_turns"); |  | 
| 54     mainDiv = doc.getElementById("main"); |  | 
| 55     frontCanvas = doc.getElementById("canvas"); |  | 
| 56     front = frontCanvas.getContext("2d"); |  | 
| 57     backCanvas = doc.createElement("canvas"); |  | 
| 58     back = backCanvas.getContext("2d"); |  | 
| 59     paletteElement = doc.getElementById("palette"); |  | 
| 60     window.addEventListener('resize', (event) => onResize(), true); |  | 
| 61     initControlPanel(); |  | 
| 62   } |  | 
| 63 |  | 
| 64   void go() { |  | 
| 65     onResize(); |  | 
| 66   } |  | 
| 67 |  | 
| 68   void onResize() { |  | 
| 69     height = window.innerHeight; |  | 
| 70     width = window.innerWidth - 270; |  | 
| 71     yc = height~/2; |  | 
| 72     xc = width~/2; |  | 
| 73     frontCanvas.height = height; |  | 
| 74     frontCanvas.width = width; |  | 
| 75     backCanvas.height = height; |  | 
| 76     backCanvas.width = width; |  | 
| 77     clear(); |  | 
| 78   } |  | 
| 79 |  | 
| 80   void initControlPanel() { |  | 
| 81     inOrOut.addEventListener('change', (event) => refresh(), true); |  | 
| 82     fixedRadiusSlider.addEventListener('change', (event) => refresh(), true); |  | 
| 83     wheelRadiusSlider.addEventListener('change', (event) => refresh(), true); |  | 
| 84     speedSlider.addEventListener('change', (event) => onSpeedChange(), true); |  | 
| 85     penRadiusSlider.addEventListener('change', (event) => refresh(), true); |  | 
| 86     penWidthSlider.addEventListener('change', (event) => onPenWidthChange(), tru
     e); |  | 
| 87     colorPicker = new ColorPicker(paletteElement); |  | 
| 88     colorPicker.addListener((String color) => onColorChange(color)); |  | 
| 89     doc.getElementById("start").addEventListener('click', (event) => start(), tr
     ue); |  | 
| 90     doc.getElementById("stop").addEventListener('click', (event) => stop(), true
     ); |  | 
| 91     doc.getElementById("clear").addEventListener('click', (event) => clear(), tr
     ue); |  | 
| 92     doc.getElementById("lucky").addEventListener('click', (event) => lucky(), tr
     ue); |  | 
| 93   } |  | 
| 94 |  | 
| 95   void onColorChange(String color) { |  | 
| 96     penColor = color; |  | 
| 97     drawFrame(rad); |  | 
| 98   } |  | 
| 99 |  | 
| 100   void onSpeedChange() { |  | 
| 101     speed = speedSlider.valueAsNumber; |  | 
| 102     stepSize = calcStepSize(); |  | 
| 103   } |  | 
| 104 |  | 
| 105   void onPenWidthChange() { |  | 
| 106     penWidth = penWidthSlider.valueAsNumber; |  | 
| 107     drawFrame(rad); |  | 
| 108   } |  | 
| 109 |  | 
| 110   void refresh() { |  | 
| 111     stop(); |  | 
| 112     // Reset |  | 
| 113     lastX = lastY = 0; |  | 
| 114     // Compute fixed radius |  | 
| 115     // based on starting diameter == min / 2, fixed radius == 10 units |  | 
| 116     int min = Math.min(height, width); |  | 
| 117     double pixelsPerUnit = min / 40; |  | 
| 118     RUnits = fixedRadiusSlider.valueAsNumber; |  | 
| 119     R = RUnits * pixelsPerUnit; |  | 
| 120     // Scale inner radius and pen distance in units of fixed radius |  | 
| 121     rUnits = wheelRadiusSlider.valueAsNumber; |  | 
| 122     r = rUnits * R/RUnits * Math.parseInt(inOrOut.value); |  | 
| 123     dUnits = penRadiusSlider.valueAsNumber; |  | 
| 124     d = dUnits * R/RUnits; |  | 
| 125     numPoints = calcNumPoints(); |  | 
| 126     maxTurns = calcTurns(); |  | 
| 127     onSpeedChange(); |  | 
| 128     numTurns.innerText = "0" + "/" + maxTurns; |  | 
| 129     penWidth = penWidthSlider.valueAsNumber; |  | 
| 130     drawFrame(0.0); |  | 
| 131   } |  | 
| 132 |  | 
| 133   int calcNumPoints() { |  | 
| 134     if ((dUnits==0) || (rUnits==0)) |  | 
| 135       // Empirically, treat it like an oval |  | 
| 136       return 2; |  | 
| 137     int gcf_ = gcf(RUnits, rUnits); |  | 
| 138     int n = RUnits ~/ gcf_; |  | 
| 139     int d_ = rUnits ~/ gcf_; |  | 
| 140     if (n % 2 == 1) |  | 
| 141       // odd |  | 
| 142       return n; |  | 
| 143     else if (d_ %2 == 1) |  | 
| 144       return n; |  | 
| 145     else |  | 
| 146       return n~/2; |  | 
| 147   } |  | 
| 148 |  | 
| 149   // TODO return optimum step size in radians |  | 
| 150   double calcStepSize() => speed / 100 * maxTurns / numPoints; |  | 
| 151 |  | 
| 152   void drawFrame(double theta) { |  | 
| 153     if (animationEnabled) { |  | 
| 154       front.clearRect(0, 0, width, height); |  | 
| 155       front.drawImage(backCanvas, 0, 0); |  | 
| 156       drawFixed(); |  | 
| 157     } |  | 
| 158     drawWheel(theta); |  | 
| 159   } |  | 
| 160 |  | 
| 161   void animate(int time) { |  | 
| 162     if (run && rad <= maxTurns * PI2) { |  | 
| 163       rad+=stepSize; |  | 
| 164       drawFrame(rad); |  | 
| 165       int nTurns = (rad / PI2).toInt(); |  | 
| 166       numTurns.innerText = '${nTurns}/$maxTurns'; |  | 
| 167       window.webkitRequestAnimationFrame(animate, frontCanvas); |  | 
| 168     } else { |  | 
| 169       stop(); |  | 
| 170     } |  | 
| 171   } |  | 
| 172 |  | 
| 173   void start() { |  | 
| 174     refresh(); |  | 
| 175     rad = 0.0; |  | 
| 176     run = true; |  | 
| 177     window.webkitRequestAnimationFrame(animate, frontCanvas); |  | 
| 178   } |  | 
| 179 |  | 
| 180   int calcTurns() { |  | 
| 181     // compute ratio of wheel radius to big R then find LCM |  | 
| 182     if ((dUnits==0) || (rUnits==0)) |  | 
| 183       return 1; |  | 
| 184     int ru = rUnits.abs(); |  | 
| 185     int wrUnits = RUnits + rUnits; |  | 
| 186     int g = gcf (wrUnits, ru); |  | 
| 187     return (ru ~/ g).toInt(); |  | 
| 188   } |  | 
| 189 |  | 
| 190   void stop() { |  | 
| 191     run = false; |  | 
| 192     // Show drawing only |  | 
| 193     front.clearRect(0, 0, width, height); |  | 
| 194     front.drawImage(backCanvas, 0, 0); |  | 
| 195     // Reset angle |  | 
| 196     rad = 0.0; |  | 
| 197   } |  | 
| 198 |  | 
| 199   void clear() { |  | 
| 200     stop(); |  | 
| 201     back.clearRect(0, 0, width, height); |  | 
| 202     refresh(); |  | 
| 203   } |  | 
| 204 |  | 
| 205   /** |  | 
| 206    * Choose random settings for wheel and pen, but |  | 
| 207    * leave fixed radius alone as it often changes |  | 
| 208    * things too much. |  | 
| 209    */ |  | 
| 210   void lucky() { |  | 
| 211     wheelRadiusSlider.valueAsNumber = Math.random() * 9; |  | 
| 212     penRadiusSlider.valueAsNumber = Math.random() * 9; |  | 
| 213     penWidthSlider.valueAsNumber = 1 + Math.random() * 9; |  | 
| 214     colorPicker.selectedColor = Math.random() * 215; |  | 
| 215     start(); |  | 
| 216   } |  | 
| 217 |  | 
| 218   void drawFixed() { |  | 
| 219     if (animationEnabled) { |  | 
| 220       front.beginPath(); |  | 
| 221       front.setLineWidth(2); |  | 
| 222       front.strokeStyle = "gray"; |  | 
| 223       front.arc(xc, yc, R, 0, PI2, true); |  | 
| 224       front.closePath(); |  | 
| 225       front.stroke(); |  | 
| 226     } |  | 
| 227   } |  | 
| 228 |  | 
| 229   /** |  | 
| 230    * Draw the wheel with its center at angle theta |  | 
| 231    * with respect to the fixed wheel |  | 
| 232    * |  | 
| 233    * @param theta |  | 
| 234    */ |  | 
| 235   void drawWheel(double theta) { |  | 
| 236     double wx = xc + ((R + r) * Math.cos(theta)); |  | 
| 237     double wy = yc - ((R + r) * Math.sin(theta)); |  | 
| 238     if (animationEnabled) { |  | 
| 239       if (rUnits>0) { |  | 
| 240         // Draw ring |  | 
| 241         front.beginPath(); |  | 
| 242         front.arc(wx, wy, r.abs(), 0, PI2, true); |  | 
| 243         front.closePath(); |  | 
| 244         front.stroke(); |  | 
| 245         // Draw center |  | 
| 246         front.setLineWidth(1); |  | 
| 247         front.beginPath(); |  | 
| 248         front.arc(wx, wy, 3, 0, PI2, true); |  | 
| 249         front.fillStyle = "black"; |  | 
| 250         front.fill(); |  | 
| 251         front.closePath(); |  | 
| 252         front.stroke(); |  | 
| 253       } |  | 
| 254     } |  | 
| 255     drawTip(wx, wy, theta); |  | 
| 256   } |  | 
| 257 |  | 
| 258   /** |  | 
| 259    * Draw a rotating line that shows the wheel rolling and leaves |  | 
| 260    * the pen trace |  | 
| 261    * |  | 
| 262    * @param wx X coordinate of wheel center |  | 
| 263    * @param wy Y coordinate of wheel center |  | 
| 264    * @param theta Angle of wheel center with respect to fixed circle |  | 
| 265    */ |  | 
| 266   void drawTip(double wx, double wy, double theta) { |  | 
| 267     // Calc wheel rotation angle |  | 
| 268     double rot = (r==0) ? theta : theta * (R+r) / r; |  | 
| 269     // Find tip of line |  | 
| 270     double tx = wx + d * Math.cos(rot); |  | 
| 271     double ty = wy - d * Math.sin(rot); |  | 
| 272     if (animationEnabled) { |  | 
| 273       front.beginPath(); |  | 
| 274       front.fillStyle = penColor; |  | 
| 275       front.arc(tx, ty, penWidth/2+2, 0, PI2, true); |  | 
| 276       front.fill(); |  | 
| 277       front.moveTo(wx, wy); |  | 
| 278       front.strokeStyle = "black"; |  | 
| 279       front.lineTo(tx, ty); |  | 
| 280       front.closePath(); |  | 
| 281       front.stroke(); |  | 
| 282     } |  | 
| 283     drawSegmentTo(tx, ty); |  | 
| 284   } |  | 
| 285 |  | 
| 286   void drawSegmentTo(double tx, double ty) { |  | 
| 287     if (lastX > 0) { |  | 
| 288       back.beginPath(); |  | 
| 289       back.strokeStyle = penColor; |  | 
| 290       back.setLineWidth(penWidth); |  | 
| 291       back.moveTo(lastX, lastY); |  | 
| 292       back.lineTo(tx, ty); |  | 
| 293       back.closePath(); |  | 
| 294       back.stroke(); |  | 
| 295     } |  | 
| 296     lastX = tx; |  | 
| 297     lastY = ty; |  | 
| 298   } |  | 
| 299 |  | 
| 300 } |  | 
| 301 |  | 
| 302 int gcf(int n, int d) { |  | 
| 303   if (n==d) |  | 
| 304     return n; |  | 
| 305   int max = Math.max(n, d); |  | 
| 306   for (int i = max ~/ 2; i > 1; i--) |  | 
| 307     if ((n % i == 0) && (d % i == 0)) |  | 
| 308       return i; |  | 
| 309   return 1; |  | 
| 310 } |  | 
| OLD | NEW | 
|---|