Implement midi UI properly, no manual redraw, have UIObject listen to model
[SugarCubes.git] / _Internals.pde
1 /**
2 * DOUBLE BLACK DIAMOND DOUBLE BLACK DIAMOND
3 *
4 * //\\ //\\ //\\ //\\
5 * ///\\\ ///\\\ ///\\\ ///\\\
6 * \\\/// \\\/// \\\/// \\\///
7 * \\// \\// \\// \\//
8 *
9 * EXPERTS ONLY!! EXPERTS ONLY!!
10 *
11 * If you are an artist, you may ignore this file! It just sets
12 * up the framework to run the patterns. Should not need modification
13 * for general animation work.
14 */
15
16 import glucose.*;
17 import glucose.control.*;
18 import glucose.effect.*;
19 import glucose.model.*;
20 import glucose.pattern.*;
21 import glucose.transform.*;
22 import glucose.transition.*;
23 import heronarts.lx.*;
24 import heronarts.lx.control.*;
25 import heronarts.lx.effect.*;
26 import heronarts.lx.modulator.*;
27 import heronarts.lx.pattern.*;
28 import heronarts.lx.transition.*;
29 import ddf.minim.*;
30 import ddf.minim.analysis.*;
31 import processing.opengl.*;
32 import rwmidi.*;
33
34 final int VIEWPORT_WIDTH = 900;
35 final int VIEWPORT_HEIGHT = 700;
36
37 // The trailer is measured from the outside of the black metal (but not including the higher welded part on the front)
38 final float TRAILER_WIDTH = 240;
39 final float TRAILER_DEPTH = 97;
40 final float TRAILER_HEIGHT = 33;
41
42 int targetFramerate = 60;
43 int startMillis, lastMillis;
44
45 // Core engine variables
46 GLucose glucose;
47 HeronLX lx;
48 LXPattern[] patterns;
49 MappingTool mappingTool;
50 PandaDriver[] pandaBoards;
51 SCMidiInput midiQwertyKeys;
52 SCMidiInput midiQwertyAPC;
53
54 // Display configuration mode
55 boolean mappingMode = false;
56 boolean debugMode = false;
57 DebugUI debugUI;
58 boolean uiOn = true;
59 LXPattern restoreToPattern = null;
60 PImage logo;
61
62 // Handles to UI objects
63 UIContext[] overlays;
64 UIPatternDeck uiPatternA;
65 UICrossfader uiCrossfader;
66 UIMidi uiMidi;
67 UIMapping uiMapping;
68 UIDebugText uiDebugText;
69
70 // Camera variables
71 float eyeR, eyeA, eyeX, eyeY, eyeZ, midX, midY, midZ;
72
73 /**
74 * Engine construction and initialization.
75 */
76 LXPattern[] _patterns(GLucose glucose) {
77 LXPattern[] patterns = patterns(glucose);
78 for (LXPattern p : patterns) {
79 p.setTransition(new DissolveTransition(glucose.lx).setDuration(1000));
80 }
81 return patterns;
82 }
83
84 void logTime(String evt) {
85 int now = millis();
86 println(evt + ": " + (now - lastMillis) + "ms");
87 lastMillis = now;
88 }
89
90 void setup() {
91 startMillis = lastMillis = millis();
92
93 // Initialize the Processing graphics environment
94 size(VIEWPORT_WIDTH, VIEWPORT_HEIGHT, OPENGL);
95 frameRate(targetFramerate);
96 noSmooth();
97 // hint(ENABLE_OPENGL_4X_SMOOTH); // no discernable improvement?
98 logTime("Created viewport");
99
100 // Create the GLucose engine to run the cubes
101 glucose = new GLucose(this, buildModel());
102 lx = glucose.lx;
103 lx.enableKeyboardTempo();
104 logTime("Built GLucose engine");
105
106 // Set the patterns
107 Engine engine = lx.engine;
108 engine.setPatterns(patterns = _patterns(glucose));
109 engine.addDeck(_patterns(glucose));
110 logTime("Built patterns");
111 glucose.setTransitions(transitions(glucose));
112 logTime("Built transitions");
113 glucose.lx.addEffects(effects(glucose));
114 logTime("Built effects");
115
116 // Build output driver
117 PandaMapping[] pandaMappings = buildPandaList();
118 pandaBoards = new PandaDriver[pandaMappings.length];
119 int pbi = 0;
120 for (PandaMapping pm : pandaMappings) {
121 pandaBoards[pbi++] = new PandaDriver(pm.ip, glucose.model, pm);
122 }
123 mappingTool = new MappingTool(glucose, pandaMappings);
124 logTime("Built PandaDriver");
125
126 // MIDI devices
127 List<SCMidiInput> midiControllers = new ArrayList<SCMidiInput>();
128 midiControllers.add(midiQwertyKeys = new SCMidiInput(SCMidiInput.KEYS));
129 midiControllers.add(midiQwertyAPC = new SCMidiInput(SCMidiInput.APC));
130 for (MidiInputDevice device : RWMidi.getInputDevices()) {
131 boolean enableDevice = device.getName().contains("APC");
132 midiControllers.add(new SCMidiInput(device).setEnabled(enableDevice));
133 }
134 SCMidiDevices.initializeStandardDevices(glucose);
135 logTime("Setup MIDI devices");
136
137 // Build overlay UI
138 debugUI = new DebugUI(pandaMappings);
139 overlays = new UIContext[] {
140 uiPatternA = new UIPatternDeck(lx.engine.getDeck(0), "PATTERN A", 4, 4, 140, 324),
141 new UIBlendMode(4, 332, 140, 86),
142 new UIEffects(4, 422, 140, 144),
143 new UITempo(4, 570, 140, 50),
144 new UISpeed(4, 624, 140, 50),
145
146 new UIPatternDeck(lx.engine.getDeck(1), "PATTERN B", width-144, 4, 140, 324),
147 uiMidi = new UIMidi(midiControllers, width-144, 332, 140, 158),
148 new UIOutput(width-144, 494, 140, 106),
149
150 uiCrossfader = new UICrossfader(width/2-90, height-90, 180, 86),
151
152 uiDebugText = new UIDebugText(148, height-138, width-304, 44),
153 uiMapping = new UIMapping(mappingTool, 4, 4, 140, 324),
154 };
155 uiMapping.setVisible(false);
156 logTime("Built overlay UI");
157
158 // Load logo image
159 logo = loadImage("data/logo.png");
160
161 // Setup camera
162 midX = TRAILER_WIDTH/2.;
163 midY = glucose.model.yMax/2;
164 midZ = TRAILER_DEPTH/2.;
165 eyeR = -290;
166 eyeA = .15;
167 eyeY = midY + 70;
168 eyeX = midX + eyeR*sin(eyeA);
169 eyeZ = midZ + eyeR*cos(eyeA);
170 addMouseWheelListener(new java.awt.event.MouseWheelListener() {
171 public void mouseWheelMoved(java.awt.event.MouseWheelEvent mwe) {
172 mouseWheel(mwe.getWheelRotation());
173 }});
174
175 println("Total setup: " + (millis() - startMillis) + "ms");
176 println("Hit the 'p' key to toggle Panda Board output");
177 }
178
179 public interface SCMidiInputListener {
180 public void onEnabled(SCMidiInput controller, boolean enabled);
181 }
182
183 public class SCMidiInput extends AbstractScrollItem {
184
185 public static final int MIDI = 0;
186 public static final int KEYS = 1;
187 public static final int APC = 2;
188
189 private boolean enabled = false;
190 private final String name;
191 private final int mode;
192 private int octaveShift = 0;
193
194 class NoteMeta {
195 int channel;
196 int number;
197 NoteMeta(int channel, int number) {
198 this.channel = channel;
199 this.number = number;
200 }
201 }
202
203 final Map<Character, NoteMeta> keyToNote = new HashMap<Character, NoteMeta>();
204
205 final List<SCMidiInputListener> listeners = new ArrayList<SCMidiInputListener>();
206
207 public SCMidiInput addListener(SCMidiInputListener l) {
208 listeners.add(l);
209 return this;
210 }
211
212 public SCMidiInput removeListener(SCMidiInputListener l) {
213 listeners.remove(l);
214 return this;
215 }
216
217 SCMidiInput(MidiInputDevice d) {
218 mode = MIDI;
219 d.createInput(this);
220 name = d.getName();
221 }
222
223 SCMidiInput(int mode) {
224 this.mode = mode;
225 switch (mode) {
226 case APC:
227 name = "QWERTY (APC Mode)";
228 mapAPC();
229 break;
230 default:
231 case KEYS:
232 name = "QWERTY (Key Mode)";
233 mapKeys();
234 break;
235 }
236 }
237
238 private void mapAPC() {
239 mapNote('1', 0, 53);
240 mapNote('2', 1, 53);
241 mapNote('3', 2, 53);
242 mapNote('4', 3, 53);
243 mapNote('5', 4, 53);
244 mapNote('6', 5, 53);
245 mapNote('q', 0, 54);
246 mapNote('w', 1, 54);
247 mapNote('e', 2, 54);
248 mapNote('r', 3, 54);
249 mapNote('t', 4, 54);
250 mapNote('y', 5, 54);
251 mapNote('a', 0, 55);
252 mapNote('s', 1, 55);
253 mapNote('d', 2, 55);
254 mapNote('f', 3, 55);
255 mapNote('g', 4, 55);
256 mapNote('h', 5, 55);
257 mapNote('z', 0, 56);
258 mapNote('x', 1, 56);
259 mapNote('c', 2, 56);
260 mapNote('v', 3, 56);
261 mapNote('b', 4, 56);
262 mapNote('n', 5, 56);
263 registerKeyEvent(this);
264 }
265
266 private void mapKeys() {
267 int note = 48;
268 mapNote('a', 1, note++);
269 mapNote('w', 1, note++);
270 mapNote('s', 1, note++);
271 mapNote('e', 1, note++);
272 mapNote('d', 1, note++);
273 mapNote('f', 1, note++);
274 mapNote('t', 1, note++);
275 mapNote('g', 1, note++);
276 mapNote('y', 1, note++);
277 mapNote('h', 1, note++);
278 mapNote('u', 1, note++);
279 mapNote('j', 1, note++);
280 mapNote('k', 1, note++);
281 mapNote('o', 1, note++);
282 mapNote('l', 1, note++);
283 registerKeyEvent(this);
284 }
285
286 void mapNote(char ch, int channel, int number) {
287 keyToNote.put(ch, new NoteMeta(channel, number));
288 }
289
290 public String getLabel() {
291 return name;
292 }
293
294 public void keyEvent(KeyEvent e) {
295 if (!enabled) {
296 return;
297 }
298 char c = Character.toLowerCase(e.getKeyChar());
299 NoteMeta nm = keyToNote.get(c);
300 if (nm != null) {
301 switch (e.getID()) {
302 case KeyEvent.KEY_PRESSED:
303 noteOnReceived(new Note(Note.NOTE_ON, nm.channel, nm.number + octaveShift*12, 127));
304 break;
305 case KeyEvent.KEY_RELEASED:
306 noteOffReceived(new Note(Note.NOTE_OFF, nm.channel, nm.number + octaveShift*12, 0));
307 break;
308 }
309 }
310 if ((mode == KEYS) && (e.getID() == KeyEvent.KEY_PRESSED)) {
311 switch (c) {
312 case 'z':
313 octaveShift = constrain(octaveShift-1, -4, 4);
314 break;
315 case 'x':
316 octaveShift = constrain(octaveShift+1, -4, 4);
317 break;
318 }
319 }
320 }
321
322 public boolean isEnabled() {
323 return enabled;
324 }
325
326 public boolean isSelected() {
327 return enabled;
328 }
329
330 public void onMousePressed() {
331 setEnabled(!enabled);
332 }
333
334 public SCMidiInput setEnabled(boolean enabled) {
335 if (enabled != this.enabled) {
336 this.enabled = enabled;
337 for (SCMidiInputListener l : listeners) {
338 l.onEnabled(this, enabled);
339 }
340 }
341 return this;
342 }
343
344 private SCPattern getFocusedPattern() {
345 Engine.Deck focusedDeck = (uiMidi != null) ? uiMidi.getFocusedDeck() : lx.engine.getDefaultDeck();
346 return (SCPattern) focusedDeck.getActivePattern();
347 }
348
349 private boolean logMidi() {
350 return (uiMidi != null) && uiMidi.logMidi();
351 }
352
353 void programChangeReceived(ProgramChange pc) {
354 if (!enabled) {
355 return;
356 }
357 if (logMidi()) {
358 println(getLabel() + " :: Program Change :: " + pc.getNumber());
359 }
360 }
361
362 void controllerChangeReceived(rwmidi.Controller cc) {
363 if (!enabled) {
364 return;
365 }
366 if (logMidi()) {
367 println(getLabel() + " :: Controller :: " + cc.getCC() + ":" + cc.getValue());
368 }
369 getFocusedPattern().controllerChangeReceived(cc);
370 }
371
372 void noteOnReceived(Note note) {
373 if (!enabled) {
374 return;
375 }
376 if (logMidi()) {
377 println(getLabel() + " :: Note On :: " + note.getChannel() + ":" + note.getPitch() + ":" + note.getVelocity());
378 }
379 getFocusedPattern().noteOnReceived(note);
380 }
381
382 void noteOffReceived(Note note) {
383 if (!enabled) {
384 return;
385 }
386 if (logMidi()) {
387 println(getLabel() + " :: Note Off :: " + note.getChannel() + ":" + note.getPitch() + ":" + note.getVelocity());
388 }
389 getFocusedPattern().noteOffReceived(note);
390 }
391
392 }
393
394 /**
395 * Core render loop and drawing functionality.
396 */
397 void draw() {
398 // Draws the simulation and the 2D UI overlay
399 background(40);
400 color[] colors = glucose.getColors();
401
402 String displayMode = uiCrossfader.getDisplayMode();
403 if (displayMode == "A") {
404 colors = lx.engine.getDeck(0).getColors();
405 } else if (displayMode == "B") {
406 colors = lx.engine.getDeck(1).getColors();
407 }
408 if (debugMode) {
409 debugUI.maskColors(colors);
410 }
411
412 camera(
413 eyeX, eyeY, eyeZ,
414 midX, midY, midZ,
415 0, -1, 0
416 );
417
418 translate(0, 40, 0);
419
420 noStroke();
421 fill(#141414);
422 drawBox(0, -TRAILER_HEIGHT, 0, 0, 0, 0, TRAILER_WIDTH, TRAILER_HEIGHT, TRAILER_DEPTH, TRAILER_HEIGHT/2.);
423 fill(#070707);
424 stroke(#222222);
425 beginShape();
426 vertex(0, 0, 0);
427 vertex(TRAILER_WIDTH, 0, 0);
428 vertex(TRAILER_WIDTH, 0, TRAILER_DEPTH);
429 vertex(0, 0, TRAILER_DEPTH);
430 endShape();
431
432 // Draw the logo on the front of platform
433 pushMatrix();
434 translate(0, 0, -1);
435 float s = .07;
436 scale(s, -s, s);
437 image(logo, TRAILER_WIDTH/2/s-logo.width/2, TRAILER_HEIGHT/2/s-logo.height/2-2/s);
438 popMatrix();
439
440 noStroke();
441 // drawBassBox(glucose.model.bassBox);
442 // for (Speaker s : glucose.model.speakers) {
443 // drawSpeaker(s);
444 // }
445 for (Cube c : glucose.model.cubes) {
446 drawCube(c);
447 }
448
449 noFill();
450 strokeWeight(2);
451 beginShape(POINTS);
452 // TODO(mcslee): restore when bassBox/speakers are right again
453 // for (Point p : glucose.model.points) {
454 for (Cube cube : glucose.model.cubes) {
455 for (Point p : cube.points) {
456 stroke(colors[p.index]);
457 vertex(p.fx, p.fy, p.fz);
458 }
459 }
460 endShape();
461
462 // 2D Overlay UI
463 drawUI();
464
465 // Send output colors
466 color[] sendColors = glucose.getColors();
467 if (debugMode) {
468 debugUI.maskColors(colors);
469 }
470
471 // Gamma correction here. Apply a cubic to the brightness
472 // for better representation of dynamic range
473 for (int i = 0; i < colors.length; ++i) {
474 float b = brightness(colors[i]) / 100.f;
475 colors[i] = color(
476 hue(colors[i]),
477 saturation(colors[i]),
478 (b*b*b) * 100.
479 );
480 }
481
482 // TODO(mcslee): move into GLucose engine
483 for (PandaDriver p : pandaBoards) {
484 p.send(colors);
485 }
486 }
487
488 void drawBassBox(BassBox b) {
489 float in = .15;
490
491 noStroke();
492 fill(#191919);
493 pushMatrix();
494 translate(b.x + BassBox.EDGE_WIDTH/2., b.y + BassBox.EDGE_HEIGHT/2, b.z + BassBox.EDGE_DEPTH/2.);
495 box(BassBox.EDGE_WIDTH-20*in, BassBox.EDGE_HEIGHT-20*in, BassBox.EDGE_DEPTH-20*in);
496 popMatrix();
497
498 noStroke();
499 fill(#393939);
500 drawBox(b.x+in, b.y+in, b.z+in, 0, 0, 0, BassBox.EDGE_WIDTH-in*2, BassBox.EDGE_HEIGHT-in*2, BassBox.EDGE_DEPTH-in*2, Cube.CHANNEL_WIDTH-in);
501
502 pushMatrix();
503 translate(b.x+(Cube.CHANNEL_WIDTH-in)/2., b.y + BassBox.EDGE_HEIGHT-in, b.z + BassBox.EDGE_DEPTH/2.);
504 float lastOffset = 0;
505 for (float offset : BoothFloor.STRIP_OFFSETS) {
506 translate(offset - lastOffset, 0, 0);
507 box(Cube.CHANNEL_WIDTH-in, 0, BassBox.EDGE_DEPTH - 2*in);
508 lastOffset = offset;
509 }
510 popMatrix();
511
512 pushMatrix();
513 translate(b.x + (Cube.CHANNEL_WIDTH-in)/2., b.y + BassBox.EDGE_HEIGHT/2., b.z + in);
514 for (int j = 0; j < 2; ++j) {
515 pushMatrix();
516 for (int i = 0; i < BassBox.NUM_FRONT_STRUTS; ++i) {
517 translate(BassBox.FRONT_STRUT_SPACING, 0, 0);
518 box(Cube.CHANNEL_WIDTH-in, BassBox.EDGE_HEIGHT - in*2, 0);
519 }
520 popMatrix();
521 translate(0, 0, BassBox.EDGE_DEPTH - 2*in);
522 }
523 popMatrix();
524
525 pushMatrix();
526 translate(b.x + in, b.y + BassBox.EDGE_HEIGHT/2., b.z + BassBox.SIDE_STRUT_SPACING + (Cube.CHANNEL_WIDTH-in)/2.);
527 box(0, BassBox.EDGE_HEIGHT - in*2, Cube.CHANNEL_WIDTH-in);
528 translate(BassBox.EDGE_WIDTH-2*in, 0, 0);
529 box(0, BassBox.EDGE_HEIGHT - in*2, Cube.CHANNEL_WIDTH-in);
530 popMatrix();
531
532 }
533
534 void drawCube(Cube c) {
535 float in = .15;
536 noStroke();
537 fill(#393939);
538 drawBox(c.x+in, c.y+in, c.z+in, c.rx, c.ry, c.rz, Cube.EDGE_WIDTH-in*2, Cube.EDGE_HEIGHT-in*2, Cube.EDGE_WIDTH-in*2, Cube.CHANNEL_WIDTH-in);
539 }
540
541 void drawSpeaker(Speaker s) {
542 float in = .15;
543
544 noStroke();
545 fill(#191919);
546 pushMatrix();
547 translate(s.x, s.y, s.z);
548 rotate(s.ry / 180. * PI, 0, -1, 0);
549 translate(Speaker.EDGE_WIDTH/2., Speaker.EDGE_HEIGHT/2., Speaker.EDGE_DEPTH/2.);
550 box(Speaker.EDGE_WIDTH-20*in, Speaker.EDGE_HEIGHT-20*in, Speaker.EDGE_DEPTH-20*in);
551 translate(0, Speaker.EDGE_HEIGHT/2. + Speaker.EDGE_HEIGHT*.8/2, 0);
552
553 fill(#222222);
554 box(Speaker.EDGE_WIDTH*.6, Speaker.EDGE_HEIGHT*.8, Speaker.EDGE_DEPTH*.75);
555 popMatrix();
556
557 noStroke();
558 fill(#393939);
559 drawBox(s.x+in, s.y+in, s.z+in, 0, s.ry, 0, Speaker.EDGE_WIDTH-in*2, Speaker.EDGE_HEIGHT-in*2, Speaker.EDGE_DEPTH-in*2, Cube.CHANNEL_WIDTH-in);
560 }
561
562 void drawBox(float x, float y, float z, float rx, float ry, float rz, float xd, float yd, float zd, float sw) {
563 pushMatrix();
564 translate(x, y, z);
565 rotate(rx / 180. * PI, -1, 0, 0);
566 rotate(ry / 180. * PI, 0, -1, 0);
567 rotate(rz / 180. * PI, 0, 0, -1);
568 for (int i = 0; i < 4; ++i) {
569 float wid = (i % 2 == 0) ? xd : zd;
570
571 beginShape();
572 vertex(0, 0);
573 vertex(wid, 0);
574 vertex(wid, yd);
575 vertex(wid - sw, yd);
576 vertex(wid - sw, sw);
577 vertex(0, sw);
578 endShape();
579 beginShape();
580 vertex(0, sw);
581 vertex(0, yd);
582 vertex(wid - sw, yd);
583 vertex(wid - sw, yd - sw);
584 vertex(sw, yd - sw);
585 vertex(sw, sw);
586 endShape();
587
588 translate(wid, 0, 0);
589 rotate(HALF_PI, 0, -1, 0);
590 }
591 popMatrix();
592 }
593
594 void drawUI() {
595 camera();
596 javax.media.opengl.GL gl = ((PGraphicsOpenGL)g).beginGL();
597 gl.glClear(javax.media.opengl.GL.GL_DEPTH_BUFFER_BIT);
598 ((PGraphicsOpenGL)g).endGL();
599 strokeWeight(1);
600
601 if (uiOn) {
602 for (UIContext context : overlays) {
603 context.draw();
604 }
605 }
606
607 // Always draw FPS meter
608 fill(#555555);
609 textSize(9);
610 textAlign(LEFT, BASELINE);
611 text("FPS: " + ((int) (frameRate*10)) / 10. + " / " + targetFramerate + " (-/+)", 4, height-4);
612
613 if (debugMode) {
614 debugUI.draw();
615 }
616 }
617
618
619 /**
620 * Top-level keyboard event handling
621 */
622 void keyPressed() {
623 if (mappingMode) {
624 mappingTool.keyPressed(uiMapping);
625 }
626 switch (key) {
627 case '-':
628 case '_':
629 frameRate(--targetFramerate);
630 break;
631 case '=':
632 case '+':
633 frameRate(++targetFramerate);
634 break;
635 case 'd':
636 if (!midiQwertyAPC.isEnabled() && !midiQwertyKeys.isEnabled()) {
637 debugMode = !debugMode;
638 println("Debug output: " + (debugMode ? "ON" : "OFF"));
639 }
640 break;
641 case 'm':
642 if (!midiQwertyAPC.isEnabled() && !midiQwertyKeys.isEnabled()) {
643 mappingMode = !mappingMode;
644 uiPatternA.setVisible(!mappingMode);
645 uiMapping.setVisible(mappingMode);
646 if (mappingMode) {
647 restoreToPattern = lx.getPattern();
648 lx.setPatterns(new LXPattern[] { mappingTool });
649 } else {
650 lx.setPatterns(patterns);
651 LXTransition pop = restoreToPattern.getTransition();
652 restoreToPattern.setTransition(null);
653 lx.goPattern(restoreToPattern);
654 restoreToPattern.setTransition(pop);
655 }
656 }
657 break;
658 case 'p':
659 for (PandaDriver p : pandaBoards) {
660 p.toggle();
661 }
662 break;
663 case 'u':
664 if (!midiQwertyAPC.isEnabled() && !midiQwertyKeys.isEnabled()) {
665 uiOn = !uiOn;
666 }
667 break;
668 }
669 }
670
671 /**
672 * Top-level mouse event handling
673 */
674 int mx, my;
675 void mousePressed() {
676 boolean debugged = false;
677 if (debugMode) {
678 debugged = debugUI.mousePressed();
679 }
680 if (!debugged) {
681 for (UIContext context : overlays) {
682 context.mousePressed(mouseX, mouseY);
683 }
684 }
685 mx = mouseX;
686 my = mouseY;
687 }
688
689 void mouseDragged() {
690 boolean dragged = false;
691 for (UIContext context : overlays) {
692 dragged |= context.mouseDragged(mouseX, mouseY);
693 }
694 if (!dragged) {
695 int dx = mouseX - mx;
696 int dy = mouseY - my;
697 mx = mouseX;
698 my = mouseY;
699 eyeA += dx*.003;
700 eyeX = midX + eyeR*sin(eyeA);
701 eyeZ = midZ + eyeR*cos(eyeA);
702 eyeY += dy;
703 }
704 }
705
706 void mouseReleased() {
707 for (UIContext context : overlays) {
708 context.mouseReleased(mouseX, mouseY);
709 }
710 }
711
712 void mouseWheel(int delta) {
713 boolean wheeled = false;
714 for (UIContext context : overlays) {
715 wheeled |= context.mouseWheel(mouseX, mouseY, delta);
716 }
717
718 if (!wheeled) {
719 eyeR = constrain(eyeR - delta, -500, -80);
720 eyeX = midX + eyeR*sin(eyeA);
721 eyeZ = midZ + eyeR*cos(eyeA);
722 }
723 }