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 |