Merge branch 'panda-refactor' of https://github.com/sugarcubes/SugarCubes
[SugarCubes.git] / _Overlay.pde
1 import java.lang.reflect.*;
2
3 /**
4 * DOUBLE BLACK DIAMOND DOUBLE BLACK DIAMOND
5 *
6 * //\\ //\\ //\\ //\\
7 * ///\\\ ///\\\ ///\\\ ///\\\
8 * \\\/// \\\/// \\\/// \\\///
9 * \\// \\// \\// \\//
10 *
11 * EXPERTS ONLY!! EXPERTS ONLY!!
12 *
13 * Overlay UI that indicates pattern control, etc. This will be moved
14 * into the Processing library once it is stabilized and need not be
15 * regularly modified.
16 */
17 abstract class OverlayUI {
18 protected final PFont titleFont = createFont("Myriad Pro", 10);
19 protected final color titleColor = #AAAAAA;
20 protected final PFont itemFont = createFont("Lucida Grande", 11);
21 protected final PFont knobFont = titleFont;
22 protected final int w = 140;
23 protected final int leftPos;
24 protected final int leftTextPos;
25 protected final int lineHeight = 20;
26 protected final int sectionSpacing = 12;
27 protected final int controlSpacing = 18;
28 protected final int tempoHeight = 20;
29 protected final int knobSize = 28;
30 protected final float knobIndent = .4;
31 protected final int knobSpacing = 6;
32 protected final int knobLabelHeight = 14;
33 protected final color lightBlue = #666699;
34 protected final color lightGreen = #669966;
35
36 private PImage logo;
37
38 protected final int STATE_DEFAULT = 0;
39 protected final int STATE_ACTIVE = 1;
40 protected final int STATE_PENDING = 2;
41
42 protected OverlayUI() {
43 leftPos = width - w;
44 leftTextPos = leftPos + 4;
45 logo = loadImage("logo-sm.png");
46 }
47
48 protected void drawLogoAndBackground() {
49 image(logo, 4, 4);
50 stroke(color(0, 0, 100));
51 // fill(color(0, 0, 50, 50)); // alpha is bad for perf
52 fill(color(0, 0, 30));
53 rect(leftPos-1, -1, w+2, height+2);
54 }
55
56 protected void drawToggleTip(String s) {
57 fill(#999999);
58 textFont(itemFont);
59 textAlign(LEFT);
60 text(s, leftTextPos, height-6);
61 }
62
63 protected void drawHelpTip() {
64 textFont(itemFont);
65 textAlign(RIGHT);
66 text("Tap 'u' to restore UI", width-4, height-6);
67 }
68
69 public void drawFPS() {
70 textFont(titleFont);
71 textAlign(LEFT);
72 fill(#666666);
73 text("FPS: " + (((int)(frameRate * 10)) / 10.), 4, height-6);
74 }
75
76 protected int drawObjectList(int yPos, String title, Object[] items, Method stateMethod) {
77 return drawObjectList(yPos, title, items, classNameArray(items, null), stateMethod);
78 }
79
80 protected int drawObjectList(int yPos, String title, Object[] items, String[] names, Method stateMethod) {
81 noStroke();
82 fill(titleColor);
83 textFont(titleFont);
84 textAlign(LEFT);
85 text(title, leftTextPos, yPos += lineHeight);
86 if (items != null) {
87 textFont(itemFont);
88 color textColor;
89 boolean even = true;
90 for (int i = 0; i < items.length; ++i) {
91 Object o = items[i];
92 int state = STATE_DEFAULT;
93 try {
94 state = ((Integer) stateMethod.invoke(this, o)).intValue();
95 } catch (Exception x) {
96 throw new RuntimeException(x);
97 }
98 switch (state) {
99 case STATE_ACTIVE:
100 fill(lightGreen);
101 textColor = #eeeeee;
102 break;
103 case STATE_PENDING:
104 fill(lightBlue);
105 textColor = color(0, 0, 75 + 15*sin(millis()/200.));;
106 break;
107 default:
108 textColor = 0;
109 fill(even ? #666666 : #777777);
110 break;
111 }
112 rect(leftPos, yPos+6, width, lineHeight);
113 fill(textColor);
114 text(names[i], leftTextPos, yPos += lineHeight);
115 even = !even;
116 }
117 }
118 return yPos;
119 }
120
121 protected String[] classNameArray(Object[] objects, String suffix) {
122 if (objects == null) {
123 return null;
124 }
125 String[] names = new String[objects.length];
126 for (int i = 0; i < objects.length; ++i) {
127 names[i] = className(objects[i], suffix);
128 }
129 return names;
130 }
131
132 protected String className(Object p, String suffix) {
133 String s = p.getClass().getName();
134 int li;
135 if ((li = s.lastIndexOf(".")) > 0) {
136 s = s.substring(li + 1);
137 }
138 if (s.indexOf("SugarCubes$") == 0) {
139 s = s.substring("SugarCubes$".length());
140 }
141 if ((suffix != null) && ((li = s.indexOf(suffix)) != -1)) {
142 s = s.substring(0, li);
143 }
144 return s;
145 }
146
147 protected int objectClickIndex(int firstItemY) {
148 return (mouseY - firstItemY) / lineHeight;
149 }
150
151 abstract public void draw();
152 abstract public void mousePressed();
153 abstract public void mouseDragged();
154 abstract public void mouseReleased();
155 }
156
157 /**
158 * UI for control of patterns, transitions, effects.
159 */
160 class ControlUI extends OverlayUI {
161 private final String[] patternNames;
162 private final String[] transitionNames;
163 private final String[] effectNames;
164
165 private int firstPatternY;
166 private int firstPatternKnobY;
167 private int firstTransitionY;
168 private int firstTransitionKnobY;
169 private int firstEffectY;
170 private int firstEffectKnobY;
171
172 private int tempoY;
173
174 private Method patternStateMethod;
175 private Method transitionStateMethod;
176 private Method effectStateMethod;
177
178 ControlUI() {
179 patternNames = classNameArray(patterns, "Pattern");
180 transitionNames = classNameArray(transitions, "Transition");
181 effectNames = classNameArray(effects, "Effect");
182
183 try {
184 patternStateMethod = getClass().getMethod("getState", LXPattern.class);
185 effectStateMethod = getClass().getMethod("getState", LXEffect.class);
186 transitionStateMethod = getClass().getMethod("getState", LXTransition.class);
187 } catch (Exception x) {
188 throw new RuntimeException(x);
189 }
190 }
191
192 public void draw() {
193 drawLogoAndBackground();
194 int yPos = 0;
195 firstPatternY = yPos + lineHeight + 6;
196 yPos = drawObjectList(yPos, "PATTERN", patterns, patternNames, patternStateMethod);
197 yPos += controlSpacing;
198 firstPatternKnobY = yPos;
199 int xPos = leftTextPos;
200 for (int i = 0; i < glucose.NUM_PATTERN_KNOBS/2; ++i) {
201 drawKnob(xPos, yPos, knobSize, glucose.patternKnobs.get(i));
202 drawKnob(xPos, yPos + knobSize + knobSpacing + knobLabelHeight, knobSize, glucose.patternKnobs.get(glucose.NUM_PATTERN_KNOBS/2 + i));
203 xPos += knobSize + knobSpacing;
204 }
205 yPos += 2*(knobSize + knobLabelHeight) + knobSpacing;
206
207 yPos += sectionSpacing;
208 firstTransitionY = yPos + lineHeight + 6;
209 yPos = drawObjectList(yPos, "TRANSITION", transitions, transitionNames, transitionStateMethod);
210 yPos += controlSpacing;
211 firstTransitionKnobY = yPos;
212 xPos = leftTextPos;
213 for (VirtualTransitionKnob knob : glucose.transitionKnobs) {
214 drawKnob(xPos, yPos, knobSize, knob);
215 xPos += knobSize + knobSpacing;
216 }
217 yPos += knobSize + knobLabelHeight;
218
219 yPos += sectionSpacing;
220 firstEffectY = yPos + lineHeight + 6;
221 yPos = drawObjectList(yPos, "FX", effects, effectNames, effectStateMethod);
222 yPos += controlSpacing;
223 firstEffectKnobY = yPos;
224 xPos = leftTextPos;
225 for (VirtualEffectKnob knob : glucose.effectKnobs) {
226 drawKnob(xPos, yPos, knobSize, knob);
227 xPos += knobSize + knobSpacing;
228 }
229 yPos += knobSize + knobLabelHeight;
230
231 yPos += sectionSpacing;
232 yPos = drawObjectList(yPos, "TEMPO", null, null, null);
233 yPos += 6;
234 tempoY = yPos;
235 stroke(#111111);
236 fill(tempoDown ? lightGreen : color(0, 0, 35 - 8*lx.tempo.rampf()));
237 rect(leftPos + 4, yPos, w - 8, tempoHeight);
238 fill(0);
239 textAlign(CENTER);
240 text("" + ((int)(lx.tempo.bpmf() * 100) / 100.), leftPos + w/2., yPos + tempoHeight - 6);
241 yPos += tempoHeight;
242
243 drawToggleTip("Tap 'u' to hide");
244 }
245
246 public LXParameter getOrNull(List<LXParameter> items, int index) {
247 if (index < items.size()) {
248 return items.get(index);
249 }
250 return null;
251 }
252
253 public int getState(LXPattern p) {
254 if (p == lx.getPattern()) {
255 return STATE_ACTIVE;
256 } else if (p == lx.getNextPattern()) {
257 return STATE_PENDING;
258 }
259 return STATE_DEFAULT;
260 }
261
262 public int getState(LXEffect e) {
263 if (e.isEnabled()) {
264 return STATE_PENDING;
265 } else if (e == glucose.getSelectedEffect()) {
266 return STATE_ACTIVE;
267 }
268 return STATE_DEFAULT;
269 }
270
271 public int getState(LXTransition t) {
272 if (t == lx.getTransition()) {
273 return STATE_PENDING;
274 } else if (t == glucose.getSelectedTransition()) {
275 return STATE_ACTIVE;
276 }
277 return STATE_DEFAULT;
278 }
279
280 private void drawKnob(int xPos, int yPos, int knobSize, LXParameter knob) {
281 final float knobValue = knob.getValuef();
282 String knobLabel = knob.getLabel();
283 if (knobLabel == null) {
284 knobLabel = "-";
285 } else if (knobLabel.length() > 4) {
286 knobLabel = knobLabel.substring(0, 4);
287 }
288
289 ellipseMode(CENTER);
290 noStroke();
291 fill(#222222);
292 // For some reason this arc call really crushes drawing performance. Presumably
293 // because openGL is drawing it and when we overlap the second set of arcs it
294 // does a bunch of depth buffer intersection tests? Ellipse with a trapezoid cut out is faster
295 // arc(xPos + knobSize/2, yPos + knobSize/2, knobSize, knobSize, HALF_PI + knobIndent, HALF_PI + knobIndent + (TWO_PI-2*knobIndent));
296 ellipse(xPos + knobSize/2, yPos + knobSize/2, knobSize, knobSize);
297
298 float endArc = HALF_PI + knobIndent + (TWO_PI-2*knobIndent)*knobValue;
299 fill(lightGreen);
300 arc(xPos + knobSize/2, yPos + knobSize/2, knobSize, knobSize, HALF_PI + knobIndent, endArc);
301
302 // Mask notch out of knob
303 fill(color(0, 0, 30));
304 beginShape();
305 vertex(xPos + knobSize/2, yPos + knobSize/2.);
306 vertex(xPos + knobSize/2 - 6, yPos + knobSize);
307 vertex(xPos + knobSize/2 + 6, yPos + knobSize);
308 endShape();
309
310 // Center circle of knob
311 fill(#333333);
312 ellipse(xPos + knobSize/2, yPos + knobSize/2, knobSize/2, knobSize/2);
313
314 fill(0);
315 rect(xPos, yPos + knobSize + 2, knobSize, knobLabelHeight - 2);
316 fill(#999999);
317 textAlign(CENTER);
318 textFont(knobFont);
319 text(knobLabel, xPos + knobSize/2, yPos + knobSize + knobLabelHeight - 2);
320 }
321
322 private int patternKnobIndex = -1;
323 private int transitionKnobIndex = -1;
324 private int effectKnobIndex = -1;
325
326 private int lastY;
327 private int releaseEffect = -1;
328 private boolean tempoDown = false;
329
330 public void mousePressed() {
331 lastY = mouseY;
332 patternKnobIndex = transitionKnobIndex = effectKnobIndex = -1;
333 releaseEffect = -1;
334 if (mouseY > tempoY) {
335 if (mouseY - tempoY < tempoHeight) {
336 lx.tempo.tap();
337 tempoDown = true;
338 }
339 } else if ((mouseY >= firstEffectKnobY) && (mouseY < firstEffectKnobY + knobSize + knobLabelHeight)) {
340 effectKnobIndex = (mouseX - leftTextPos) / (knobSize + knobSpacing);
341 } else if (mouseY > firstEffectY) {
342 int effectIndex = objectClickIndex(firstEffectY);
343 if (effectIndex < effects.length) {
344 if (effects[effectIndex] == glucose.getSelectedEffect()) {
345 effects[effectIndex].enable();
346 releaseEffect = effectIndex;
347 }
348 glucose.setSelectedEffect(effectIndex);
349 }
350 } else if ((mouseY >= firstTransitionKnobY) && (mouseY < firstTransitionKnobY + knobSize + knobLabelHeight)) {
351 transitionKnobIndex = (mouseX - leftTextPos) / (knobSize + knobSpacing);
352 } else if (mouseY > firstTransitionY) {
353 int transitionIndex = objectClickIndex(firstTransitionY);
354 if (transitionIndex < transitions.length) {
355 glucose.setSelectedTransition(transitionIndex);
356 }
357 } else if ((mouseY >= firstPatternKnobY) && (mouseY < firstPatternKnobY + 2*(knobSize+knobLabelHeight) + knobSpacing)) {
358 patternKnobIndex = (mouseX - leftTextPos) / (knobSize + knobSpacing);
359 if (mouseY >= firstPatternKnobY + knobSize + knobLabelHeight + knobSpacing) {
360 patternKnobIndex += glucose.NUM_PATTERN_KNOBS / 2;
361 }
362 } else if (mouseY > firstPatternY) {
363 int patternIndex = objectClickIndex(firstPatternY);
364 if (patternIndex < patterns.length) {
365 lx.goIndex(patternIndex);
366 }
367 }
368 }
369
370 public void mouseDragged() {
371 int dy = lastY - mouseY;
372 lastY = mouseY;
373 if (patternKnobIndex >= 0 && patternKnobIndex < glucose.NUM_PATTERN_KNOBS) {
374 LXParameter p = glucose.patternKnobs.get(patternKnobIndex);
375 p.setValue(constrain(p.getValuef() + dy*.01, 0, 1));
376 } else if (effectKnobIndex >= 0 && effectKnobIndex < glucose.NUM_EFFECT_KNOBS) {
377 LXParameter p = glucose.effectKnobs.get(effectKnobIndex);
378 p.setValue(constrain(p.getValuef() + dy*.01, 0, 1));
379 } else if (transitionKnobIndex >= 0 && transitionKnobIndex < glucose.NUM_TRANSITION_KNOBS) {
380 LXParameter p = glucose.transitionKnobs.get(transitionKnobIndex);
381 p.setValue(constrain(p.getValuef() + dy*.01, 0, 1));
382 }
383 }
384
385 public void mouseReleased() {
386 tempoDown = false;
387 if (releaseEffect >= 0) {
388 effects[releaseEffect].trigger();
389 releaseEffect = -1;
390 }
391 }
392
393 }
394
395 /**
396 * UI for control of mapping.
397 */
398 class MappingUI extends OverlayUI {
399
400 private MappingTool mappingTool;
401
402 private final String MAPPING_MODE_ALL = "All On";
403 private final String MAPPING_MODE_CHANNEL = "Channel";
404 private final String MAPPING_MODE_SINGLE_CUBE = "Single Cube";
405
406 private final String[] mappingModes = {
407 MAPPING_MODE_ALL,
408 MAPPING_MODE_CHANNEL,
409 MAPPING_MODE_SINGLE_CUBE
410 };
411 private final Method mappingModeStateMethod;
412
413 private final String CUBE_MODE_ALL = "All Strips";
414 private final String CUBE_MODE_SINGLE_STRIP = "Single Strip";
415 private final String CUBE_MODE_STRIP_PATTERN = "Strip Pattern";
416 private final String[] cubeModes = {
417 CUBE_MODE_ALL,
418 CUBE_MODE_SINGLE_STRIP,
419 CUBE_MODE_STRIP_PATTERN
420 };
421 private final Method cubeModeStateMethod;
422
423 private final String CHANNEL_MODE_RED = "Red";
424 private final String CHANNEL_MODE_GREEN = "Green";
425 private final String CHANNEL_MODE_BLUE = "Blue";
426 private final String[] channelModes = {
427 CHANNEL_MODE_RED,
428 CHANNEL_MODE_GREEN,
429 CHANNEL_MODE_BLUE,
430 };
431 private final Method channelModeStateMethod;
432
433 private int firstMappingY;
434 private int firstCubeY;
435 private int firstChannelY;
436 private int channelFieldY;
437 private int cubeFieldY;
438 private int stripFieldY;
439
440 private boolean dragCube;
441 private boolean dragStrip;
442 private boolean dragChannel;
443
444 MappingUI(MappingTool mappingTool) {
445 this.mappingTool = mappingTool;
446 try {
447 mappingModeStateMethod = getClass().getMethod("getMappingState", Object.class);
448 channelModeStateMethod = getClass().getMethod("getChannelState", Object.class);
449 cubeModeStateMethod = getClass().getMethod("getCubeState", Object.class);
450 } catch (Exception x) {
451 throw new RuntimeException(x);
452 }
453 }
454
455 public int getMappingState(Object mappingMode) {
456 boolean active = false;
457 if (mappingMode == MAPPING_MODE_ALL) {
458 active = mappingTool.mappingMode == mappingTool.MAPPING_MODE_ALL;
459 } else if (mappingMode == MAPPING_MODE_CHANNEL) {
460 active = mappingTool.mappingMode == mappingTool.MAPPING_MODE_CHANNEL;
461 } else if (mappingMode == MAPPING_MODE_SINGLE_CUBE) {
462 active = mappingTool.mappingMode == mappingTool.MAPPING_MODE_SINGLE_CUBE;
463 }
464 return active ? STATE_ACTIVE : STATE_DEFAULT;
465 }
466
467 public int getChannelState(Object channelMode) {
468 boolean active = false;
469 if (channelMode == CHANNEL_MODE_RED) {
470 active = mappingTool.channelModeRed;
471 } else if (channelMode == CHANNEL_MODE_GREEN) {
472 active = mappingTool.channelModeGreen;
473 } else if (channelMode == CHANNEL_MODE_BLUE) {
474 active = mappingTool.channelModeBlue;
475 }
476 return active ? STATE_ACTIVE : STATE_DEFAULT;
477 }
478
479 public int getCubeState(Object cubeMode) {
480 boolean active = false;
481 if (cubeMode == CUBE_MODE_ALL) {
482 active = mappingTool.cubeMode == mappingTool.CUBE_MODE_ALL;
483 } else if (cubeMode == CUBE_MODE_SINGLE_STRIP) {
484 active = mappingTool.cubeMode == mappingTool.CUBE_MODE_SINGLE_STRIP;
485 } else if (cubeMode == CUBE_MODE_STRIP_PATTERN) {
486 active = mappingTool.cubeMode == mappingTool.CUBE_MODE_STRIP_PATTERN;
487 }
488 return active ? STATE_ACTIVE : STATE_DEFAULT;
489 }
490
491 public void draw() {
492 drawLogoAndBackground();
493 int yPos = 0;
494 firstMappingY = yPos + lineHeight + 6;
495 yPos = drawObjectList(yPos, "MAPPING MODE", mappingModes, mappingModes, mappingModeStateMethod);
496 yPos += sectionSpacing;
497
498 firstCubeY = yPos + lineHeight + 6;
499 yPos = drawObjectList(yPos, "CUBE MODE", cubeModes, cubeModes, cubeModeStateMethod);
500 yPos += sectionSpacing;
501
502 firstChannelY = yPos + lineHeight + 6;
503 yPos = drawObjectList(yPos, "CHANNELS", channelModes, channelModes, channelModeStateMethod);
504 yPos += sectionSpacing;
505
506 channelFieldY = yPos + lineHeight + 6;
507 yPos = drawValueField(yPos, "CHANNEL ID", mappingTool.channelIndex + 1);
508 yPos += sectionSpacing;
509
510 cubeFieldY = yPos + lineHeight + 6;
511 yPos = drawValueField(yPos, "CUBE ID", glucose.model.getRawIndexForCube(mappingTool.cubeIndex));
512 yPos += sectionSpacing;
513
514 stripFieldY = yPos + lineHeight + 6;
515 yPos = drawValueField(yPos, "STRIP ID", mappingTool.stripIndex + 1);
516
517 drawToggleTip("Tap 'm' to return");
518 }
519
520 private int drawValueField(int yPos, String label, int value) {
521 yPos += lineHeight;
522 textAlign(LEFT);
523 textFont(titleFont);
524 fill(titleColor);
525 text(label, leftTextPos, yPos);
526 fill(0);
527 yPos += 6;
528 rect(leftTextPos, yPos, w-8, lineHeight);
529 yPos += lineHeight;
530
531 fill(#999999);
532 textAlign(CENTER);
533 textFont(itemFont);
534 text("" + value, leftTextPos + (w-8)/2, yPos - 5);
535
536 return yPos;
537 }
538
539 private int lastY;
540
541 public void mousePressed() {
542 dragCube = dragStrip = dragChannel = false;
543 lastY = mouseY;
544 if (mouseY >= stripFieldY) {
545 if (mouseY < stripFieldY + lineHeight) {
546 dragStrip = true;
547 }
548 } else if (mouseY >= cubeFieldY) {
549 if (mouseY < cubeFieldY + lineHeight) {
550 dragCube = true;
551 }
552 } else if (mouseY >= channelFieldY) {
553 if (mouseY < channelFieldY + lineHeight) {
554 dragChannel = true;
555 }
556 } else if (mouseY >= firstChannelY) {
557 int index = objectClickIndex(firstChannelY);
558 switch (index) {
559 case 0: mappingTool.channelModeRed = !mappingTool.channelModeRed; break;
560 case 1: mappingTool.channelModeGreen = !mappingTool.channelModeGreen; break;
561 case 2: mappingTool.channelModeBlue = !mappingTool.channelModeBlue; break;
562 }
563 } else if (mouseY >= firstCubeY) {
564 int index = objectClickIndex(firstCubeY);
565 switch (index) {
566 case 0: mappingTool.cubeMode = mappingTool.CUBE_MODE_ALL; break;
567 case 1: mappingTool.cubeMode = mappingTool.CUBE_MODE_SINGLE_STRIP; break;
568 case 2: mappingTool.cubeMode = mappingTool.CUBE_MODE_STRIP_PATTERN; break;
569 }
570 } else if (mouseY >= firstMappingY) {
571 int index = objectClickIndex(firstMappingY);
572 switch (index) {
573 case 0: mappingTool.mappingMode = mappingTool.MAPPING_MODE_ALL; break;
574 case 1: mappingTool.mappingMode = mappingTool.MAPPING_MODE_CHANNEL; break;
575 case 2: mappingTool.mappingMode = mappingTool.MAPPING_MODE_SINGLE_CUBE; break;
576 }
577 }
578 }
579
580 public void mouseReleased() {
581 }
582
583 public void mouseDragged() {
584 final int DRAG_THRESHOLD = 5;
585 int dy = lastY - mouseY;
586 if (abs(dy) >= DRAG_THRESHOLD) {
587 lastY = mouseY;
588 if (dragCube) {
589 if (dy < 0) {
590 mappingTool.decCube();
591 } else {
592 mappingTool.incCube();
593 }
594 } else if (dragStrip) {
595 if (dy < 0) {
596 mappingTool.decStrip();
597 } else {
598 mappingTool.incStrip();
599 }
600 } else if (dragChannel) {
601 if (dy < 0) {
602 mappingTool.decChannel();
603 } else {
604 mappingTool.incChannel();
605 }
606 }
607 }
608
609 }
610
611
612 }
613
614 void mousePressed() {
615 if (mouseX > ui.leftPos) {
616 ui.mousePressed();
617 }
618 }
619
620 void mouseReleased() {
621 if (mouseX > ui.leftPos) {
622 ui.mouseReleased();
623 }
624 }
625
626 void mouseDragged() {
627 if (mouseX > ui.leftPos) {
628 ui.mouseDragged();
629 }
630 }
631