2 * DOUBLE BLACK DIAMOND DOUBLE BLACK DIAMOND
5 * ///\\\ ///\\\ ///\\\ ///\\\
6 * \\\/// \\\/// \\\/// \\\///
9 * EXPERTS ONLY!! EXPERTS ONLY!!
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.
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.*;
30 import ddf.minim.analysis.*;
31 import processing.opengl.*;
34 final int VIEWPORT_WIDTH = 900;
35 final int VIEWPORT_HEIGHT = 700;
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;
42 int targetFramerate = 60;
43 int startMillis, lastMillis;
45 // Core engine variables
49 MappingTool mappingTool;
50 PandaDriver[] pandaBoards;
51 MidiListener midiQwerty;
53 // Display configuration mode
54 boolean mappingMode = false;
55 boolean debugMode = false;
58 LXPattern restoreToPattern = null;
60 // Handles to UI objects
62 UIPatternDeck uiPatternA;
63 UICrossfader uiCrossfader;
66 UIDebugText uiDebugText;
69 float eyeR, eyeA, eyeX, eyeY, eyeZ, midX, midY, midZ;
72 * Engine construction and initialization.
74 LXPattern[] _patterns(GLucose glucose) {
75 LXPattern[] patterns = patterns(glucose);
76 for (LXPattern p : patterns) {
77 p.setTransition(new DissolveTransition(glucose.lx).setDuration(1000));
82 void logTime(String evt) {
84 println(evt + ": " + (now - lastMillis) + "ms");
89 startMillis = lastMillis = millis();
91 // Initialize the Processing graphics environment
92 size(VIEWPORT_WIDTH, VIEWPORT_HEIGHT, OPENGL);
93 frameRate(targetFramerate);
95 // hint(ENABLE_OPENGL_4X_SMOOTH); // no discernable improvement?
96 logTime("Created viewport");
98 // Create the GLucose engine to run the cubes
99 glucose = new GLucose(this, buildModel());
101 lx.enableKeyboardTempo();
102 logTime("Built GLucose engine");
105 Engine engine = lx.engine;
106 engine.setPatterns(patterns = _patterns(glucose));
107 engine.addDeck(_patterns(glucose));
108 logTime("Built patterns");
109 glucose.setTransitions(transitions(glucose));
110 logTime("Built transitions");
111 glucose.lx.addEffects(effects(glucose));
112 logTime("Built effects");
114 // Build output driver
115 PandaMapping[] pandaMappings = buildPandaList();
116 pandaBoards = new PandaDriver[pandaMappings.length];
118 for (PandaMapping pm : pandaMappings) {
119 pandaBoards[pbi++] = new PandaDriver(pm.ip, glucose.model, pm);
121 mappingTool = new MappingTool(glucose, pandaMappings);
122 logTime("Built PandaDriver");
125 List<MidiListener> midiListeners = new ArrayList<MidiListener>();
126 midiListeners.add(midiQwerty = new MidiListener());
127 for (MidiInputDevice device : RWMidi.getInputDevices()) {
128 boolean enableDevice = device.getName().contains("APC");
129 midiListeners.add(new MidiListener(device).setEnabled(enableDevice));
131 SCMidiDevices.initializeStandardDevices(glucose);
132 logTime("Setup MIDI devices");
135 debugUI = new DebugUI(pandaMappings);
136 overlays = new UIContext[] {
137 uiPatternA = new UIPatternDeck(lx.engine.getDeck(0), "PATTERN A", 4, 4, 140, 324),
138 new UIBlendMode(4, 332, 140, 86),
139 new UIEffects(4, 422, 140, 144),
140 new UITempo(4, 570, 140, 50),
141 new UISpeed(4, 624, 140, 50),
143 new UIPatternDeck(lx.engine.getDeck(1), "PATTERN B", width-144, 4, 140, 324),
144 uiMidi = new UIMidi(midiListeners, width-144, 332, 140, 136),
145 new UIOutput(width-144, 472, 140, 106),
147 uiCrossfader = new UICrossfader(width/2-90, height-90, 180, 86),
149 uiDebugText = new UIDebugText(148, height-138, width-304, 44),
150 uiMapping = new UIMapping(mappingTool, 4, 4, 140, 324),
152 uiMapping.setVisible(false);
153 logTime("Built overlay UI");
156 midX = TRAILER_WIDTH/2.;
157 midY = glucose.model.yMax/2;
158 midZ = TRAILER_DEPTH/2.;
162 eyeX = midX + eyeR*sin(eyeA);
163 eyeZ = midZ + eyeR*cos(eyeA);
164 addMouseWheelListener(new java.awt.event.MouseWheelListener() {
165 public void mouseWheelMoved(java.awt.event.MouseWheelEvent mwe) {
166 mouseWheel(mwe.getWheelRotation());
169 println("Total setup: " + (millis() - startMillis) + "ms");
170 println("Hit the 'p' key to toggle Panda Board output");
173 public class MidiListener extends AbstractScrollItem {
174 private boolean enabled = false;
175 private final String name;
177 MidiListener(MidiInputDevice d) {
185 NoteMeta(int channel, int number) {
186 this.channel = channel;
187 this.number = number;
191 final Map<Character, NoteMeta> keyToNote = new HashMap<Character, NoteMeta>();
194 name = "QWERTY Keyboard";
219 registerKeyEvent(this);
222 void mapNote(char ch, int channel, int number) {
223 keyToNote.put(ch, new NoteMeta(channel, number));
226 public String getLabel() {
230 public void keyEvent(KeyEvent e) {
231 char c = Character.toLowerCase(e.getKeyChar());
232 NoteMeta nm = keyToNote.get(c);
235 case KeyEvent.KEY_PRESSED:
236 noteOnReceived(new Note(Note.NOTE_ON, nm.channel, nm.number, 127));
238 case KeyEvent.KEY_RELEASED:
239 noteOffReceived(new Note(Note.NOTE_OFF, nm.channel, nm.number, 0));
245 public boolean isEnabled() {
249 public boolean isSelected() {
253 public void onMousePressed() {
254 setEnabled(!enabled);
257 public MidiListener setEnabled(boolean enabled) {
258 if (enabled != this.enabled) {
259 this.enabled = enabled;
265 private SCPattern getFocusedPattern() {
266 return (SCPattern) uiMidi.getFocusedDeck().getActivePattern();
269 void programChangeReceived(ProgramChange pc) {
273 println("PC: " + pc.toString());
276 void controllerChangeReceived(rwmidi.Controller cc) {
280 println("CC: " + cc.toString());
281 getFocusedPattern().controllerChangeReceived(cc);
284 void noteOnReceived(Note note) {
288 println("Note On: " + note.toString());
289 getFocusedPattern().noteOnReceived(note);
293 void noteOffReceived(Note note) {
297 println("Note Off: " + note.toString());
298 getFocusedPattern().noteOffReceived(note);
304 * Core render loop and drawing functionality.
307 // Draws the simulation and the 2D UI overlay
309 color[] colors = glucose.getColors();
311 String displayMode = uiCrossfader.getDisplayMode();
312 if (displayMode == "A") {
313 colors = lx.engine.getDeck(0).getColors();
314 } else if (displayMode == "B") {
315 colors = lx.engine.getDeck(1).getColors();
318 debugUI.maskColors(colors);
331 drawBox(0, -TRAILER_HEIGHT, 0, 0, 0, 0, TRAILER_WIDTH, TRAILER_HEIGHT, TRAILER_DEPTH, TRAILER_HEIGHT/2.);
336 vertex(TRAILER_WIDTH, 0, 0);
337 vertex(TRAILER_WIDTH, 0, TRAILER_DEPTH);
338 vertex(0, 0, TRAILER_DEPTH);
342 // drawBassBox(glucose.model.bassBox);
343 // for (Speaker s : glucose.model.speakers) {
346 for (Cube c : glucose.model.cubes) {
353 // TODO(mcslee): restore when bassBox/speakers are right again
354 // for (Point p : glucose.model.points) {
355 for (Cube cube : glucose.model.cubes) {
356 for (Point p : cube.points) {
357 stroke(colors[p.index]);
358 vertex(p.fx, p.fy, p.fz);
366 // Send output colors
367 color[] sendColors = glucose.getColors();
369 debugUI.maskColors(colors);
372 // Gamma correction here. Apply a cubic to the brightness
373 // for better representation of dynamic range
374 for (int i = 0; i < colors.length; ++i) {
375 float b = brightness(colors[i]) / 100.f;
378 saturation(colors[i]),
383 // TODO(mcslee): move into GLucose engine
384 for (PandaDriver p : pandaBoards) {
389 void drawBassBox(BassBox b) {
395 translate(b.x + BassBox.EDGE_WIDTH/2., b.y + BassBox.EDGE_HEIGHT/2, b.z + BassBox.EDGE_DEPTH/2.);
396 box(BassBox.EDGE_WIDTH-20*in, BassBox.EDGE_HEIGHT-20*in, BassBox.EDGE_DEPTH-20*in);
401 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);
404 translate(b.x+(Cube.CHANNEL_WIDTH-in)/2., b.y + BassBox.EDGE_HEIGHT-in, b.z + BassBox.EDGE_DEPTH/2.);
405 float lastOffset = 0;
406 for (float offset : BoothFloor.STRIP_OFFSETS) {
407 translate(offset - lastOffset, 0, 0);
408 box(Cube.CHANNEL_WIDTH-in, 0, BassBox.EDGE_DEPTH - 2*in);
414 translate(b.x + (Cube.CHANNEL_WIDTH-in)/2., b.y + BassBox.EDGE_HEIGHT/2., b.z + in);
415 for (int j = 0; j < 2; ++j) {
417 for (int i = 0; i < BassBox.NUM_FRONT_STRUTS; ++i) {
418 translate(BassBox.FRONT_STRUT_SPACING, 0, 0);
419 box(Cube.CHANNEL_WIDTH-in, BassBox.EDGE_HEIGHT - in*2, 0);
422 translate(0, 0, BassBox.EDGE_DEPTH - 2*in);
427 translate(b.x + in, b.y + BassBox.EDGE_HEIGHT/2., b.z + BassBox.SIDE_STRUT_SPACING + (Cube.CHANNEL_WIDTH-in)/2.);
428 box(0, BassBox.EDGE_HEIGHT - in*2, Cube.CHANNEL_WIDTH-in);
429 translate(BassBox.EDGE_WIDTH-2*in, 0, 0);
430 box(0, BassBox.EDGE_HEIGHT - in*2, Cube.CHANNEL_WIDTH-in);
435 void drawCube(Cube c) {
439 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);
442 void drawSpeaker(Speaker s) {
448 translate(s.x, s.y, s.z);
449 rotate(s.ry / 180. * PI, 0, -1, 0);
450 translate(Speaker.EDGE_WIDTH/2., Speaker.EDGE_HEIGHT/2., Speaker.EDGE_DEPTH/2.);
451 box(Speaker.EDGE_WIDTH-20*in, Speaker.EDGE_HEIGHT-20*in, Speaker.EDGE_DEPTH-20*in);
452 translate(0, Speaker.EDGE_HEIGHT/2. + Speaker.EDGE_HEIGHT*.8/2, 0);
455 box(Speaker.EDGE_WIDTH*.6, Speaker.EDGE_HEIGHT*.8, Speaker.EDGE_DEPTH*.75);
460 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);
463 void drawBox(float x, float y, float z, float rx, float ry, float rz, float xd, float yd, float zd, float sw) {
466 rotate(rx / 180. * PI, -1, 0, 0);
467 rotate(ry / 180. * PI, 0, -1, 0);
468 rotate(rz / 180. * PI, 0, 0, -1);
469 for (int i = 0; i < 4; ++i) {
470 float wid = (i % 2 == 0) ? xd : zd;
476 vertex(wid - sw, yd);
477 vertex(wid - sw, sw);
483 vertex(wid - sw, yd);
484 vertex(wid - sw, yd - sw);
489 translate(wid, 0, 0);
490 rotate(HALF_PI, 0, -1, 0);
497 javax.media.opengl.GL gl = ((PGraphicsOpenGL)g).beginGL();
498 gl.glClear(javax.media.opengl.GL.GL_DEPTH_BUFFER_BIT);
499 ((PGraphicsOpenGL)g).endGL();
503 for (UIContext context : overlays) {
508 // Always draw FPS meter
511 textAlign(LEFT, BASELINE);
512 text("FPS: " + ((int) (frameRate*10)) / 10. + " / " + targetFramerate + " (-/+)", 4, height-4);
520 * Top-level keyboard event handling
524 mappingTool.keyPressed(uiMapping);
529 frameRate(--targetFramerate);
533 frameRate(++targetFramerate);
536 if (!midiQwerty.isEnabled()) {
537 debugMode = !debugMode;
538 println("Debug output: " + (debugMode ? "ON" : "OFF"));
542 if (!midiQwerty.isEnabled()) {
543 mappingMode = !mappingMode;
544 uiPatternA.setVisible(!mappingMode);
545 uiMapping.setVisible(mappingMode);
547 restoreToPattern = lx.getPattern();
548 lx.setPatterns(new LXPattern[] { mappingTool });
550 lx.setPatterns(patterns);
551 LXTransition pop = restoreToPattern.getTransition();
552 restoreToPattern.setTransition(null);
553 lx.goPattern(restoreToPattern);
554 restoreToPattern.setTransition(pop);
559 for (PandaDriver p : pandaBoards) {
570 * Top-level mouse event handling
573 void mousePressed() {
574 boolean debugged = false;
576 debugged = debugUI.mousePressed();
579 for (UIContext context : overlays) {
580 context.mousePressed(mouseX, mouseY);
587 void mouseDragged() {
588 boolean dragged = false;
589 for (UIContext context : overlays) {
590 dragged |= context.mouseDragged(mouseX, mouseY);
593 int dx = mouseX - mx;
594 int dy = mouseY - my;
598 eyeX = midX + eyeR*sin(eyeA);
599 eyeZ = midZ + eyeR*cos(eyeA);
604 void mouseReleased() {
605 for (UIContext context : overlays) {
606 context.mouseReleased(mouseX, mouseY);
610 void mouseWheel(int delta) {
611 boolean wheeled = false;
612 for (UIContext context : overlays) {
613 wheeled |= context.mouseWheel(mouseX, mouseY, delta);
617 eyeR = constrain(eyeR - delta, -500, -80);
618 eyeX = midX + eyeR*sin(eyeA);
619 eyeZ = midZ + eyeR*cos(eyeA);