| 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 |