From d626bc9b0197a1b5fd51a86f33f666a2a46579a2 Mon Sep 17 00:00:00 2001 From: Mark Slee Date: Mon, 16 Sep 2013 18:07:06 -0700 Subject: [PATCH] Overhaul UI code to make it more flexible and less redrawing, little hierarchy framework --- DanKaminsky.pde | 6 +- DanUtil.pde | 6 +- MarkSlee.pde | 3 + SugarCubes.pde | 4 +- TestPatterns.pde | 27 +- _DebugUI.pde | 283 ++++++++++++ _Internals.pde | 154 +++--- _Overlay.pde | 1028 ----------------------------------------- _PandaDriver.pde | 70 +-- _UIFramework.pde | 825 +++++++++++++++++++++++++++++++++ _UIImplementation.pde | 450 ++++++++++++++++++ code/GLucose.jar | Bin 25782 -> 26495 bytes code/HeronLX.jar | Bin 61255 -> 63879 bytes 13 files changed, 1735 insertions(+), 1121 deletions(-) mode change 100755 => 100644 DanUtil.pde mode change 100755 => 100644 MarkSlee.pde mode change 100755 => 100644 TestPatterns.pde create mode 100644 _DebugUI.pde delete mode 100644 _Overlay.pde create mode 100644 _UIFramework.pde create mode 100644 _UIImplementation.pde diff --git a/DanKaminsky.pde b/DanKaminsky.pde index 59bde73..3fbd86b 100644 --- a/DanKaminsky.pde +++ b/DanKaminsky.pde @@ -215,7 +215,7 @@ import processing.serial.*; List gparams; -class DualBlender extends SCEffect { + class DualBlender extends SCEffect { int lastSeen; BasicParameter p1 = new BasicParameter("p1", 0); BasicParameter p2 = new BasicParameter("p2", 0); @@ -250,9 +250,9 @@ class DualBlender extends SCEffect { if(p==p7) { gparams.get(6).setValue(p.getValuef()); } if(p==p8) { gparams.get(7).setValue(p.getValuef()); } } - + void doApply(int[] colors){ - if(doDual==true){ + if (enabled) { //gplay.onActive(); gplay.go(millis()-lastSeen); lastSeen=millis(); diff --git a/DanUtil.pde b/DanUtil.pde old mode 100755 new mode 100644 index d9990dd..9577673 --- a/DanUtil.pde +++ b/DanUtil.pde @@ -80,7 +80,7 @@ public class DPat extends SCPattern int unmapRow (int a) { return btwn(a,0 , 4) ? a+53 : a; } void SetLight (int row, int col, int clr){ if (midiout != null) midiout.sendNoteOn(col, unmapRow(row), clr); } void keypad (int row, int col) { println(row + " " + col); } - void onInactive() { bIsActive=false; DanTextLine1 = ""; DanTextLine2 = "";} + void onInactive() { bIsActive=false; DanTextLine1 = ""; DanTextLine2 = ""; uiDebugText.setText(""); } void onActive () { bIsActive=true; zSpinHue = 0; for (int i=0; i 0); break; + case MAPPING_MODE_CHANNEL: cubeOn = (indexOfCubeInChannel > 0); break; } if (cubeOn) { if (mappingMode == MAPPING_MODE_CHANNEL) { @@ -408,7 +412,10 @@ class MappingTool extends TestPattern { } ++ci; } - + } + + public void setCube(int index) { + cubeIndex = index % model.cubes.size(); } public void incCube() { @@ -421,6 +428,11 @@ class MappingTool extends TestPattern { cubeIndex += model.cubes.size(); } } + + public void setChannel(int index) { + channelIndex = index % numChannels; + setChannel(); + } public void incChannel() { channelIndex = (channelIndex + 1) % numChannels; @@ -432,6 +444,10 @@ class MappingTool extends TestPattern { setChannel(); } + public void setStrip(int index) { + stripIndex = index % Cube.STRIPS_PER_CUBE; + } + public void incStrip() { stripIndex = (stripIndex + 1) % Cube.STRIPS_PER_CUBE; } @@ -440,7 +456,7 @@ class MappingTool extends TestPattern { stripIndex = (stripIndex + Cube.STRIPS_PER_CUBE - 1) % Cube.STRIPS_PER_CUBE; } - public void keyPressed() { + public void keyPressed(UIMapping uiMapping) { switch (keyCode) { case UP: if (mappingMode == MAPPING_MODE_CHANNEL) incChannel(); else incCube(); break; case DOWN: if (mappingMode == MAPPING_MODE_CHANNEL) decChannel(); else decCube(); break; @@ -452,5 +468,10 @@ class MappingTool extends TestPattern { case 'g': channelModeGreen = !channelModeGreen; break; case 'b': channelModeBlue = !channelModeBlue; break; } + uiMapping.setChannelID(channelIndex+1); + uiMapping.setCubeID(cubeIndex+1); + uiMapping.setStripID(stripIndex+1); + uiMapping.redraw(); } + } diff --git a/_DebugUI.pde b/_DebugUI.pde new file mode 100644 index 0000000..5b99d43 --- /dev/null +++ b/_DebugUI.pde @@ -0,0 +1,283 @@ +/** + * DOUBLE BLACK DIAMOND DOUBLE BLACK DIAMOND + * + * //\\ //\\ //\\ //\\ + * ///\\\ ///\\\ ///\\\ ///\\\ + * \\\/// \\\/// \\\/// \\\/// + * \\// \\// \\// \\// + * + * EXPERTS ONLY!! EXPERTS ONLY!! + * + * Overlay UI that indicates pattern control, etc. This will be moved + * into the Processing library once it is stabilized and need not be + * regularly modified. + */ + +class DebugUI { + + final ChannelMapping[] channelList; + final int debugX = 5; + final int debugY = 5; + final int debugXSpacing = 28; + final int debugYSpacing = 21; + final int[][] debugState; + final int[] indexState; + + final int CUBE_STATE_UNUSED = 0; + final int CUBE_STATE_USED = 1; + final int CUBE_STATE_DUPLICATED = 2; + + final int DEBUG_STATE_ANIM = 0; + final int DEBUG_STATE_WHITE = 1; + final int DEBUG_STATE_OFF = 2; + final int DEBUG_STATE_UNUSED = 3; + + DebugUI(PandaMapping[] pandaMappings) { + int totalChannels = pandaMappings.length * PandaMapping.CHANNELS_PER_BOARD; + debugState = new int[totalChannels+1][ChannelMapping.CUBES_PER_CHANNEL+1]; + indexState = new int[glucose.model.cubes.size()+1]; + + channelList = new ChannelMapping[totalChannels]; + int channelIndex = 0; + for (PandaMapping pm : pandaMappings) { + for (ChannelMapping channel : pm.channelList) { + channelList[channelIndex++] = channel; + } + } + for (int i = 0; i < debugState.length; ++i) { + for (int j = 0; j < debugState[i].length; ++j) { + debugState[i][j] = DEBUG_STATE_ANIM; + } + } + + for (int rawIndex = 0; rawIndex < glucose.model.cubes.size()+1; ++rawIndex) { + indexState[rawIndex] = CUBE_STATE_UNUSED; + } + for (ChannelMapping channel : channelList) { + for (int rawCubeIndex : channel.objectIndices) { + if (rawCubeIndex > 0) + ++indexState[rawCubeIndex]; + } + } + } + + void draw() { + noStroke(); + int xBase = debugX; + int yPos = debugY; + + textSize(10); + + fill(#000000); + rect(0, 0, debugX + 5*debugXSpacing, height); + + int channelNum = 0; + for (ChannelMapping channel : channelList) { + int xPos = xBase; + drawNumBox(xPos, yPos, channelNum+1, debugState[channelNum][0]); + xPos += debugXSpacing; + + switch (channel.mode) { + case ChannelMapping.MODE_CUBES: + int stateIndex = 0; + boolean first = true; + for (int rawCubeIndex : channel.objectIndices) { + if (rawCubeIndex < 0) { + break; + } + if (first) { + first = false; + } else { + stroke(#999999); + line(xPos - 12, yPos + 8, xPos, yPos + 8); + } + drawNumBox(xPos, yPos, rawCubeIndex, debugState[channelNum][stateIndex+1], indexState[rawCubeIndex]); + ++stateIndex; + xPos += debugXSpacing; + } + break; + case ChannelMapping.MODE_BASS: + drawNumBox(xPos, yPos, "B", debugState[channelNum][1]); + break; + case ChannelMapping.MODE_SPEAKER: + drawNumBox(xPos, yPos, "S" + channel.objectIndices[0], debugState[channelNum][1]); + break; + case ChannelMapping.MODE_STRUTS_AND_FLOOR: + drawNumBox(xPos, yPos, "F", debugState[channelNum][1]); + break; + case ChannelMapping.MODE_NULL: + break; + default: + throw new RuntimeException("Unhandled channel mapping mode: " + channel.mode); + } + + yPos += debugYSpacing; + ++channelNum; + } + drawNumBox(xBase, yPos, "A", debugState[channelNum][0]); + yPos += debugYSpacing * 2; + + noFill(); + fill(#CCCCCC); + text("Unused Cubes", xBase, yPos + 12); + yPos += debugYSpacing; + + int xIndex = 0; + for (int rawIndex = 1; rawIndex <= glucose.model.cubes.size(); ++rawIndex) { + if (indexState[rawIndex] == CUBE_STATE_UNUSED) { + drawNumBox(xBase + (xIndex * debugXSpacing), yPos, rawIndex, DEBUG_STATE_UNUSED); + ++xIndex; + if (xIndex > 4) { + xIndex = 0; + yPos += debugYSpacing + 2; + } + } + } + } + + + void drawNumBox(int xPos, int yPos, int label, int state) { + drawNumBox(xPos, yPos, "" + label, state); + } + + void drawNumBox(int xPos, int yPos, String label, int state) { + drawNumBox(xPos, yPos, "" + label, state, CUBE_STATE_USED); + } + + void drawNumBox(int xPos, int yPos, int label, int state, int cubeState) { + drawNumBox(xPos, yPos, "" + label, state, cubeState); + } + + void drawNumBox(int xPos, int yPos, String label, int state, int cubeState) { + noFill(); + color textColor = #cccccc; + switch (state) { + case DEBUG_STATE_ANIM: + noStroke(); + fill(#880000); + rect(xPos, yPos, 16, 8); + fill(#000088); + rect(xPos, yPos+8, 16, 8); + noFill(); + stroke(textColor); + break; + case DEBUG_STATE_WHITE: + stroke(textColor); + fill(#e9e9e9); + textColor = #333333; + break; + case DEBUG_STATE_OFF: + stroke(textColor); + break; + case DEBUG_STATE_UNUSED: + stroke(textColor); + fill(#880000); + break; + } + + if (cubeState >= CUBE_STATE_DUPLICATED) { + stroke(textColor = #FF0000); + } + + rect(xPos, yPos, 16, 16); + noStroke(); + fill(textColor); + text(label, xPos + 2, yPos + 12); + } + + void maskColors(color[] colors) { + color white = #FFFFFF; + color off = #000000; + int channelIndex = 0; + int state; + for (ChannelMapping channel : channelList) { + switch (channel.mode) { + case ChannelMapping.MODE_CUBES: + int cubeIndex = 1; + for (int rawCubeIndex : channel.objectIndices) { + if (rawCubeIndex >= 0) { + state = debugState[channelIndex][cubeIndex]; + if (state != DEBUG_STATE_ANIM) { + color debugColor = (state == DEBUG_STATE_WHITE) ? white : off; + Cube cube = glucose.model.getCubeByRawIndex(rawCubeIndex); + for (Point p : cube.points) { + colors[p.index] = debugColor; + } + } + } + ++cubeIndex; + } + break; + + case ChannelMapping.MODE_BASS: + state = debugState[channelIndex][1]; + if (state != DEBUG_STATE_ANIM) { + color debugColor = (state == DEBUG_STATE_WHITE) ? white : off; + for (Strip s : glucose.model.bassBox.boxStrips) { + for (Point p : s.points) { + colors[p.index] = debugColor; + } + } + } + break; + + case ChannelMapping.MODE_STRUTS_AND_FLOOR: + state = debugState[channelIndex][1]; + if (state != DEBUG_STATE_ANIM) { + color debugColor = (state == DEBUG_STATE_WHITE) ? white : off; + for (Point p : glucose.model.boothFloor.points) { + colors[p.index] = debugColor; + } + for (Strip s : glucose.model.bassBox.struts) { + for (Point p : s.points) { + colors[p.index] = debugColor; + } + } + } + break; + + case ChannelMapping.MODE_SPEAKER: + state = debugState[channelIndex][1]; + if (state != DEBUG_STATE_ANIM) { + color debugColor = (state == DEBUG_STATE_WHITE) ? white : off; + for (Point p : glucose.model.speakers.get(channel.objectIndices[0]).points) { + colors[p.index] = debugColor; + } + } + break; + + case ChannelMapping.MODE_NULL: + break; + + default: + throw new RuntimeException("Unhandled channel mapping mode: " + channel.mode); + } + ++channelIndex; + } + } + + boolean mousePressed() { + int dx = (mouseX - debugX) / debugXSpacing; + int dy = (mouseY - debugY) / debugYSpacing; + if ((dy < 0) || (dy >= debugState.length)) { + return false; + } + if ((dx < 0) || (dx >= debugState[dy].length)) { + return false; + } + int newState = debugState[dy][dx] = (debugState[dy][dx] + 1) % 3; + if (dy == debugState.length-1) { + for (int[] states : debugState) { + for (int i = 0; i < states.length; ++i) { + states[i] = newState; + } + } + } else if (dx == 0) { + for (int i = 0; i < debugState[dy].length; ++i) { + debugState[dy][i] = newState; + } + } + return true; + } +} + diff --git a/_Internals.pde b/_Internals.pde index 5035ed5..57a0161 100644 --- a/_Internals.pde +++ b/_Internals.pde @@ -48,17 +48,28 @@ MappingTool mappingTool; LXPattern[] patterns; LXTransition[] transitions; LXEffect[] effects; -OverlayUI ui; -ControlUI controlUI; -MappingUI mappingUI; PandaDriver[] pandaBoards; boolean mappingMode = false; boolean debugMode = false; DebugUI debugUI; +String displayMode; + +UIContext[] overlays; +UIPatternDeck uiPatternA; +UIMapping uiMapping; +UIDebugText uiDebugText; // Camera variables float eyeR, eyeA, eyeX, eyeY, eyeZ, midX, midY, midZ; +LXPattern[] _patterns(GLucose glucose) { + LXPattern[] patterns = patterns(glucose); + for (LXPattern p : patterns) { + p.setTransition(new DissolveTransition(glucose.lx).setDuration(1000)); + } + return patterns; +} + void setup() { startMillis = lastMillis = millis(); @@ -76,12 +87,15 @@ void setup() { logTime("Built GLucose engine"); // Set the patterns - glucose.lx.setPatterns(patterns = patterns(glucose)); + Engine engine = lx.engine; + glucose.setTransitions(transitions = transitions(glucose)); + logTime("Built transitions"); + engine.setPatterns(patterns = _patterns(glucose)); + engine.addDeck(_patterns(glucose)); + engine.getDeck(1).setBlendTransition(transitions[0]); logTime("Built patterns"); glucose.lx.addEffects(effects = effects(glucose)); logTime("Built effects"); - glucose.setTransitions(transitions = transitions(glucose)); - logTime("Built transitions"); // Build output driver PandaMapping[] pandaMappings = buildPandaList(); @@ -94,9 +108,20 @@ void setup() { logTime("Built PandaDriver"); // Build overlay UI - ui = controlUI = new ControlUI(); - mappingUI = new MappingUI(mappingTool); debugUI = new DebugUI(pandaMappings); + overlays = new UIContext[] { + uiPatternA = new UIPatternDeck(lx.engine.getDeck(0), "PATTERN A", 4, 4, 140, 344), + new UICrossfader(4, 352, 140, 152), + new UIOutput(4, 508, 140, 122), + + new UIPatternDeck(lx.engine.getDeck(1), "PATTERN B", width-144, 4, 140, 344), + new UIEffects(width-144, 352, 140, 144), + new UITempo(width-144, 498, 140, 50), + + uiDebugText = new UIDebugText(4, height-64, width-8, 44), + uiMapping = new UIMapping(mappingTool, 4, 4, 140, 344), + }; + uiMapping.setVisible(false); logTime("Built overlay UI"); // MIDI devices @@ -107,12 +132,12 @@ void setup() { logTime("Setup MIDI devices"); // Setup camera - midX = TRAILER_WIDTH/2. + 20; + midX = TRAILER_WIDTH/2.; midY = glucose.model.yMax/2; midZ = TRAILER_DEPTH/2.; eyeR = -290; eyeA = .15; - eyeY = midY + 20; + eyeY = midY + 70; eyeX = midX + eyeR*sin(eyeA); eyeZ = midZ + eyeR*cos(eyeA); addMouseWheelListener(new java.awt.event.MouseWheelListener() { @@ -152,7 +177,12 @@ void logTime(String evt) { void draw() { // Draws the simulation and the 2D UI overlay background(40); - color[] colors = glucose.getColors(); + color[] colors = glucose.getColors();; + if (displayMode == "A") { + colors = lx.engine.getDeck(0).getColors(); + } else if (displayMode == "B") { + colors = lx.engine.getDeck(1).getColors(); + } if (debugMode) { debugUI.maskColors(colors); } @@ -163,6 +193,8 @@ void draw() { 0, -1, 0 ); + translate(0, 10, 0); + noStroke(); fill(#141414); drawBox(0, -TRAILER_HEIGHT, 0, 0, 0, 0, TRAILER_WIDTH, TRAILER_HEIGHT, TRAILER_DEPTH, TRAILER_HEIGHT/2.); @@ -197,16 +229,13 @@ void draw() { } endShape(); - // 2D Overlay - camera(); - javax.media.opengl.GL gl = ((PGraphicsOpenGL)g).beginGL(); - gl.glClear(javax.media.opengl.GL.GL_DEPTH_BUFFER_BIT); - ((PGraphicsOpenGL)g).endGL(); - strokeWeight(1); + // 2D Overlay UI drawUI(); - + + // Send output colors + color[] sendColors = glucose.getColors(); if (debugMode) { - debugUI.draw(); + debugUI.maskColors(colors); } // Gamma correction here. Apply a cubic to the brightness @@ -333,28 +362,37 @@ void drawBox(float x, float y, float z, float rx, float ry, float rz, float xd, } void drawUI() { + camera(); + javax.media.opengl.GL gl = ((PGraphicsOpenGL)g).beginGL(); + gl.glClear(javax.media.opengl.GL.GL_DEPTH_BUFFER_BIT); + ((PGraphicsOpenGL)g).endGL(); + strokeWeight(1); + if (uiOn) { - ui.draw(); - } else { - ui.drawHelpTip(); + for (UIContext context : overlays) { + context.draw(); + } + } + + // Always draw FPS meter + fill(#555555); + textSize(9); + textAlign(LEFT, BASELINE); + text("FPS: " + ((int) (frameRate*10)) / 10. + " / " + targetFramerate + " (-/+)", 4, height-4); + + if (debugMode) { + debugUI.draw(); } - ui.drawFPS(); - ui.drawDanText(); } boolean uiOn = true; -int restoreToIndex = -1; - -boolean doDual = false; +LXPattern restoreToPattern = null; void keyPressed() { if (mappingMode) { - mappingTool.keyPressed(); + mappingTool.keyPressed(uiMapping); } switch (key) { - case 'w': - doDual = !doDual; - break; case '-': case '_': frameRate(--targetFramerate); @@ -369,20 +407,14 @@ void keyPressed() { break; case 'm': mappingMode = !mappingMode; + uiPatternA.setVisible(!mappingMode); + uiMapping.setVisible(mappingMode); if (mappingMode) { - LXPattern pattern = lx.getPattern(); - for (int i = 0; i < patterns.length; ++i) { - if (pattern == patterns[i]) { - restoreToIndex = i; - break; - } - } - ui = mappingUI; + restoreToPattern = lx.getPattern(); lx.setPatterns(new LXPattern[] { mappingTool }); } else { - ui = controlUI; lx.setPatterns(patterns); - lx.goIndex(restoreToIndex); + lx.goPattern(restoreToPattern); } break; case 'p': @@ -398,20 +430,25 @@ void keyPressed() { int mx, my; void mousePressed() { - ui.mousePressed(); - if (mouseX < ui.leftPos) { - if (debugMode) { - debugUI.mousePressed(); - } - mx = mouseX; - my = mouseY; + boolean debugged = false; + if (debugMode) { + debugged = debugUI.mousePressed(); + } + if (!debugged) { + for (UIContext context : overlays) { + context.mousePressed(mouseX, mouseY); + } } + mx = mouseX; + my = mouseY; } void mouseDragged() { - if (mouseX > ui.leftPos) { - ui.mouseDragged(); - } else { + boolean dragged = false; + for (UIContext context : overlays) { + dragged |= context.mouseDragged(mouseX, mouseY); + } + if (!dragged) { int dx = mouseX - mx; int dy = mouseY - my; mx = mouseX; @@ -424,13 +461,20 @@ void mouseDragged() { } void mouseReleased() { - ui.mouseReleased(); + for (UIContext context : overlays) { + context.mouseReleased(mouseX, mouseY); + } + + // ui.mouseReleased(); } void mouseWheel(int delta) { - if (mouseX > ui.leftPos) { - ui.mouseWheel(delta); - } else { + boolean wheeled = false; + for (UIContext context : overlays) { + wheeled |= context.mouseWheel(mouseX, mouseY, delta); + } + + if (!wheeled) { eyeR = constrain(eyeR - delta, -500, -80); eyeX = midX + eyeR*sin(eyeA); eyeZ = midZ + eyeR*cos(eyeA); diff --git a/_Overlay.pde b/_Overlay.pde deleted file mode 100644 index 1b13b03..0000000 --- a/_Overlay.pde +++ /dev/null @@ -1,1028 +0,0 @@ -import java.lang.reflect.*; - -/** - * DOUBLE BLACK DIAMOND DOUBLE BLACK DIAMOND - * - * //\\ //\\ //\\ //\\ - * ///\\\ ///\\\ ///\\\ ///\\\ - * \\\/// \\\/// \\\/// \\\/// - * \\// \\// \\// \\// - * - * EXPERTS ONLY!! EXPERTS ONLY!! - * - * Overlay UI that indicates pattern control, etc. This will be moved - * into the Processing library once it is stabilized and need not be - * regularly modified. - */ -abstract class OverlayUI { - protected final PFont titleFont = createFont("Myriad Pro", 10); - protected final color titleColor = #AAAAAA; - protected final PFont itemFont = createFont("Lucida Grande", 11); - protected final PFont knobFont = titleFont; - protected final int w = 140; - protected final int leftPos; - protected final int leftTextPos; - protected final int lineHeight = 20; - protected final int sectionSpacing = 12; - protected final int controlSpacing = 18; - protected final int tempoHeight = 20; - protected final int knobSize = 28; - protected final float knobIndent = .4; - protected final int knobSpacing = 6; - protected final int knobLabelHeight = 14; - protected final int scrollWidth = 14; - protected final color lightBlue = #666699; - protected final color lightGreen = #669966; - protected final int toggleButtonSize = 10; - - private PImage logo; - - protected final int STATE_DEFAULT = 0; - protected final int STATE_ACTIVE = 1; - protected final int STATE_PENDING = 2; - - protected int[] pandaLeft = new int[pandaBoards.length]; - protected final int pandaWidth = 64; - protected final int pandaHeight = 13; - protected final int pandaTop = height-16; - - protected int eligibleLeft; - - protected OverlayUI() { - leftPos = width - w; - leftTextPos = leftPos + 4; - logo = loadImage("logo-sm.png"); - } - - protected void drawLogoAndBackground() { - image(logo, 4, 4); - stroke(color(0, 0, 100)); - // fill(color(0, 0, 50, 50)); // alpha is bad for perf - fill(color(0, 0, 30)); - rect(leftPos-1, -1, w+2, height+2); - } - - protected void drawToggleTip(String s) { - fill(#999999); - textFont(itemFont); - textAlign(LEFT); - text(s, leftTextPos, height-6); - } - - protected void drawHelpTip() { - textFont(itemFont); - textAlign(RIGHT); - text("Tap 'u' to restore UI", width-4, height-6); - } - - public void drawDanText() { - textFont(itemFont); - textAlign(LEFT); - fill(#FFFFFF); - text(DanTextLine1, 4, height-50); - text(DanTextLine2, 4, height-30); - } - - public void drawFPS() { - textFont(titleFont); - textAlign(LEFT); - noStroke(); - fill(#666666); - int lPos = 4; - String fps = "FPS: " + (((int)(frameRate * 10)) / 10.); - text(fps, lPos, height-6); - lPos += 48; - - String target = "Target (-/+):"; - text(target, lPos, height-6); - fill(#000000); - lPos += textWidth(target) + 4; - rect(lPos, height-16, 24, 13); - fill(#666666); - text("" + targetFramerate, lPos + (24 - textWidth("" + targetFramerate))/2, height-6); - lPos += 32; - String pandaOutput = "PandaOutput (p):"; - text(pandaOutput, lPos, height-6); - lPos += textWidth(pandaOutput)+4; - int pi = 0; - for (PandaDriver p : pandaBoards) { - pandaLeft[pi++] = lPos; - fill(p.enabled ? #666666 : #000000); - rect(lPos, pandaTop, pandaWidth, pandaHeight); - fill(p.enabled ? #000000 : #666666); - text(p.ip, lPos + (pandaWidth - textWidth(p.ip)) / 2, height-6); - lPos += pandaWidth + 8; - } - - } - - protected int drawObjectList(int yPos, String title, Object[] items, Method stateMethod) { - int sz = (items != null) ? items.length : 0; - return drawObjectList(yPos, title, items, stateMethod, sz, 0); - } - - protected int drawObjectList(int yPos, String title, Object[] items, Method stateMethod, int scrollLength, int scrollPos) { - return drawObjectList(yPos, title, items, classNameArray(items, null), stateMethod, scrollLength, scrollPos); - } - - protected int drawObjectList(int yPos, String title, Object[] items, String[] names, Method stateMethod) { - int sz = (items != null) ? items.length : 0; - return drawObjectList(yPos, title, items, names, stateMethod, sz, 0); - } - - protected void drawToggleButton(float x, float y, boolean eligible, color textColor) { - noFill(); - stroke(textColor); - rect(x, y, toggleButtonSize, toggleButtonSize); - if (eligible) { - noStroke(); - fill(textColor); - rect(x + 2, y + 2, toggleButtonSize - 4, toggleButtonSize - 4); - } - } - - protected int drawObjectList(int yPos, String title, Object[] items, String[] names, Method stateMethod, int scrollLength, int scrollPos) { - noStroke(); - fill(titleColor); - textFont(titleFont); - textAlign(LEFT); - text(title, leftTextPos, yPos += lineHeight); - if (items != null) { - boolean hasScroll = (scrollPos > 0) || (scrollLength < items.length); - textFont(itemFont); - color textColor; - boolean even = true; - int yTop = yPos+6; - for (int i = scrollPos; i < items.length && i < (scrollPos + scrollLength); ++i) { - Object o = items[i]; - int state = STATE_DEFAULT; - try { - state = ((Integer) stateMethod.invoke(this, o)).intValue(); - } catch (Exception x) { - throw new RuntimeException(x); - } - switch (state) { - case STATE_ACTIVE: - fill(lightGreen); - textColor = #eeeeee; - break; - case STATE_PENDING: - fill(lightBlue); - textColor = color(0, 0, 75 + 15*sin(millis()/200.));; - break; - default: - textColor = 0; - fill(even ? #666666 : #777777); - break; - } - noStroke(); - rect(leftPos, yPos+6, w, lineHeight); - fill(textColor); - text(names[i], leftTextPos, yPos += lineHeight); - if (lx.isAutoTransitionEnabled() && items[i] instanceof LXPattern) { - boolean eligible = ((LXPattern)items[i]).isEligible(); - eligibleLeft = leftPos + w - (hasScroll ? scrollWidth : 0) - 15; - drawToggleButton(eligibleLeft, yPos-8, eligible, textColor); - } - even = !even; - } - if (hasScroll) { - int yHere = yPos+6; - noStroke(); - fill(color(0, 0, 0, 50)); - rect(leftPos + w - scrollWidth, yTop, scrollWidth, yHere - yTop); - fill(#666666); - rect(leftPos + w - scrollWidth + 2, yTop + (yHere-yTop) * (scrollPos / (float)items.length), scrollWidth - 4, (yHere - yTop) * (scrollLength / (float)items.length)); - - } - - } - return yPos; - } - - protected String[] classNameArray(Object[] objects, String suffix) { - if (objects == null) { - return null; - } - String[] names = new String[objects.length]; - for (int i = 0; i < objects.length; ++i) { - names[i] = className(objects[i], suffix); - } - return names; - } - - protected String className(Object p, String suffix) { - String s = p.getClass().getName(); - int li; - if ((li = s.lastIndexOf(".")) > 0) { - s = s.substring(li + 1); - } - if (s.indexOf("SugarCubes$") == 0) { - s = s.substring("SugarCubes$".length()); - } - if ((suffix != null) && ((li = s.indexOf(suffix)) != -1)) { - s = s.substring(0, li); - } - return s; - } - - protected int objectClickIndex(int firstItemY) { - return (mouseY - firstItemY) / lineHeight; - } - - abstract public void draw(); - abstract public void mousePressed(); - abstract public void mouseDragged(); - abstract public void mouseReleased(); - abstract public void mouseWheel(int delta); -} - -/** - * UI for control of patterns, transitions, effects. - */ -class ControlUI extends OverlayUI { - private final String[] patternNames; - private final String[] transitionNames; - private final String[] effectNames; - - private int firstPatternY; - private int firstPatternKnobY; - private int firstTransitionY; - private int firstTransitionKnobY; - private int firstEffectY; - private int firstEffectKnobY; - - private int autoRotateX; - private int autoRotateY; - - private final int PATTERN_LIST_LENGTH = 8; - private int patternScrollPos = 0; - - private int tempoY; - - private Method patternStateMethod; - private Method transitionStateMethod; - private Method effectStateMethod; - - ControlUI() { - patternNames = classNameArray(patterns, "Pattern"); - transitionNames = classNameArray(transitions, "Transition"); - effectNames = classNameArray(effects, "Effect"); - - try { - patternStateMethod = getClass().getMethod("getState", LXPattern.class); - effectStateMethod = getClass().getMethod("getState", LXEffect.class); - transitionStateMethod = getClass().getMethod("getState", LXTransition.class); - } catch (Exception x) { - throw new RuntimeException(x); - } - } - - public void draw() { - drawLogoAndBackground(); - int yPos = 0; - autoRotateX = leftPos + w - 29; - autoRotateY = yPos + 12; - drawToggleButton(autoRotateX, autoRotateY, lx.isAutoTransitionEnabled(), #999999); - fill(lx.isAutoTransitionEnabled() ? #222222: #999999); - text("A", autoRotateX + 2, autoRotateY + 9); - firstPatternY = yPos + lineHeight + 6; - yPos = drawObjectList(yPos, "PATTERN", patterns, patternNames, patternStateMethod, PATTERN_LIST_LENGTH, patternScrollPos); - yPos += controlSpacing; - firstPatternKnobY = yPos; - int xPos = leftTextPos; - for (int i = 0; i < glucose.NUM_PATTERN_KNOBS/2; ++i) { - drawKnob(xPos, yPos, knobSize, glucose.patternKnobs.get(i)); - drawKnob(xPos, yPos + knobSize + knobSpacing + knobLabelHeight, knobSize, glucose.patternKnobs.get(glucose.NUM_PATTERN_KNOBS/2 + i)); - xPos += knobSize + knobSpacing; - } - yPos += 2*(knobSize + knobLabelHeight) + knobSpacing; - - yPos += sectionSpacing; - firstTransitionY = yPos + lineHeight + 6; - yPos = drawObjectList(yPos, "TRANSITION", transitions, transitionNames, transitionStateMethod); - yPos += controlSpacing; - firstTransitionKnobY = yPos; - xPos = leftTextPos; - for (VirtualTransitionKnob knob : glucose.transitionKnobs) { - drawKnob(xPos, yPos, knobSize, knob); - xPos += knobSize + knobSpacing; - } - yPos += knobSize + knobLabelHeight; - - yPos += sectionSpacing; - firstEffectY = yPos + lineHeight + 6; - yPos = drawObjectList(yPos, "FX", effects, effectNames, effectStateMethod); - yPos += controlSpacing; - firstEffectKnobY = yPos; - xPos = leftTextPos; - for (VirtualEffectKnob knob : glucose.effectKnobs) { - drawKnob(xPos, yPos, knobSize, knob); - xPos += knobSize + knobSpacing; - } - yPos += knobSize + knobLabelHeight; - - yPos += sectionSpacing; - yPos = drawObjectList(yPos, "TEMPO", null, null, null); - yPos += 6; - tempoY = yPos; - stroke(#111111); - fill(tempoDown ? lightGreen : color(0, 0, 35 - 8*lx.tempo.rampf())); - rect(leftPos + 4, yPos, w - 8, tempoHeight); - fill(0); - textAlign(CENTER); - text("" + ((int)(lx.tempo.bpmf() * 100) / 100.), leftPos + w/2., yPos + tempoHeight - 6); - yPos += tempoHeight; - - drawToggleTip("Tap 'u' to hide"); - } - - public LXParameter getOrNull(List items, int index) { - if (index < items.size()) { - return items.get(index); - } - return null; - } - - public int getState(LXPattern p) { - if (p == lx.getPattern()) { - return STATE_ACTIVE; - } else if (p == lx.getNextPattern()) { - return STATE_PENDING; - } - return STATE_DEFAULT; - } - - public int getState(LXEffect e) { - if (e.isEnabled()) { - return STATE_PENDING; - } else if (e == glucose.getSelectedEffect()) { - return STATE_ACTIVE; - } - return STATE_DEFAULT; - } - - public int getState(LXTransition t) { - if (t == lx.getTransition()) { - return STATE_PENDING; - } else if (t == glucose.getSelectedTransition()) { - return STATE_ACTIVE; - } - return STATE_DEFAULT; - } - - private void drawKnob(int xPos, int yPos, int knobSize, LXParameter knob) { - final float knobValue = knob.getValuef(); - String knobLabel = knob.getLabel(); - if (knobLabel == null) { - knobLabel = "-"; - } else if (knobLabel.length() > 4) { - knobLabel = knobLabel.substring(0, 4); - } - - ellipseMode(CENTER); - noStroke(); - fill(#222222); - // For some reason this arc call really crushes drawing performance. Presumably - // because openGL is drawing it and when we overlap the second set of arcs it - // does a bunch of depth buffer intersection tests? Ellipse with a trapezoid cut out is faster - // arc(xPos + knobSize/2, yPos + knobSize/2, knobSize, knobSize, HALF_PI + knobIndent, HALF_PI + knobIndent + (TWO_PI-2*knobIndent)); - ellipse(xPos + knobSize/2, yPos + knobSize/2, knobSize, knobSize); - - float endArc = HALF_PI + knobIndent + (TWO_PI-2*knobIndent)*knobValue; - fill(lightGreen); - arc(xPos + knobSize/2, yPos + knobSize/2, knobSize, knobSize, HALF_PI + knobIndent, endArc); - - // Mask notch out of knob - fill(color(0, 0, 30)); - beginShape(); - vertex(xPos + knobSize/2, yPos + knobSize/2.); - vertex(xPos + knobSize/2 - 6, yPos + knobSize); - vertex(xPos + knobSize/2 + 6, yPos + knobSize); - endShape(); - - // Center circle of knob - fill(#333333); - ellipse(xPos + knobSize/2, yPos + knobSize/2, knobSize/2, knobSize/2); - - fill(0); - rect(xPos, yPos + knobSize + 2, knobSize, knobLabelHeight - 2); - fill(#999999); - textAlign(CENTER); - textFont(knobFont); - text(knobLabel, xPos + knobSize/2, yPos + knobSize + knobLabelHeight - 2); - } - - private int patternKnobIndex = -1; - private int transitionKnobIndex = -1; - private int effectKnobIndex = -1; - private boolean patternScrolling = false; - - private int lastY; - private int releaseEffect = -1; - private boolean tempoDown = false; - - public void mousePressed() { - lastY = mouseY; - patternKnobIndex = transitionKnobIndex = effectKnobIndex = -1; - releaseEffect = -1; - patternScrolling = false; - - for (int p = 0; p < pandaLeft.length; ++p) { - int xp = pandaLeft[p]; - if ((mouseX >= xp) && - (mouseX < xp + pandaWidth) && - (mouseY >= pandaTop) && - (mouseY < pandaTop + pandaHeight)) { - pandaBoards[p].toggle(); - } - } - - if (mouseX < leftPos) { - return; - } - - if ((mouseX >= autoRotateX) && - (mouseX < autoRotateX + toggleButtonSize) && - (mouseY >= autoRotateY) && - (mouseY < autoRotateY + toggleButtonSize)) { - if (lx.isAutoTransitionEnabled()) { - lx.disableAutoTransition(); - println("Auto pattern transition disabled"); - } else { - lx.enableAutoTransition(60000); - println("Auto pattern transition enabled"); - } - return; - } - - if (mouseY > tempoY) { - if (mouseY - tempoY < tempoHeight) { - lx.tempo.tap(); - tempoDown = true; - } - } else if ((mouseY >= firstEffectKnobY) && (mouseY < firstEffectKnobY + knobSize + knobLabelHeight)) { - effectKnobIndex = (mouseX - leftTextPos) / (knobSize + knobSpacing); - } else if (mouseY > firstEffectY) { - int effectIndex = objectClickIndex(firstEffectY); - if (effectIndex < effects.length) { - if (effects[effectIndex] == glucose.getSelectedEffect()) { - effects[effectIndex].enable(); - releaseEffect = effectIndex; - } - glucose.setSelectedEffect(effectIndex); - } - } else if ((mouseY >= firstTransitionKnobY) && (mouseY < firstTransitionKnobY + knobSize + knobLabelHeight)) { - transitionKnobIndex = (mouseX - leftTextPos) / (knobSize + knobSpacing); - } else if (mouseY > firstTransitionY) { - int transitionIndex = objectClickIndex(firstTransitionY); - if (transitionIndex < transitions.length) { - glucose.setSelectedTransition(transitionIndex); - } - } else if ((mouseY >= firstPatternKnobY) && (mouseY < firstPatternKnobY + 2*(knobSize+knobLabelHeight) + knobSpacing)) { - patternKnobIndex = (mouseX - leftTextPos) / (knobSize + knobSpacing); - if (mouseY >= firstPatternKnobY + knobSize + knobLabelHeight + knobSpacing) { - patternKnobIndex += glucose.NUM_PATTERN_KNOBS / 2; - } - } else if (mouseY > firstPatternY) { - if ((patterns.length > PATTERN_LIST_LENGTH) && (mouseX > width - scrollWidth)) { - patternScrolling = true; - } else { - int patternIndex = objectClickIndex(firstPatternY); - if (patternIndex < patterns.length) { - if (lx.isAutoTransitionEnabled() && (mouseX > eligibleLeft)) { - patterns[patternIndex + patternScrollPos].toggleEligible(); - } else { - lx.goIndex(patternIndex + patternScrollPos); - } - } - } - } - } - - int scrolldy = 0; - public void mouseDragged() { - int dy = lastY - mouseY; - scrolldy += dy; - lastY = mouseY; - if (patternKnobIndex >= 0 && patternKnobIndex < glucose.NUM_PATTERN_KNOBS) { - LXParameter p = glucose.patternKnobs.get(patternKnobIndex); - p.setValue(constrain(p.getValuef() + dy*.01, 0, 1)); - } else if (effectKnobIndex >= 0 && effectKnobIndex < glucose.NUM_EFFECT_KNOBS) { - LXParameter p = glucose.effectKnobs.get(effectKnobIndex); - p.setValue(constrain(p.getValuef() + dy*.01, 0, 1)); - } else if (transitionKnobIndex >= 0 && transitionKnobIndex < glucose.NUM_TRANSITION_KNOBS) { - LXParameter p = glucose.transitionKnobs.get(transitionKnobIndex); - p.setValue(constrain(p.getValuef() + dy*.01, 0, 1)); - } else if (patternScrolling) { - int scroll = scrolldy / lineHeight; - scrolldy = scrolldy % lineHeight; - patternScrollPos = constrain(patternScrollPos - scroll, 0, patterns.length - PATTERN_LIST_LENGTH); - } - } - - public void mouseReleased() { - patternScrolling = false; - tempoDown = false; - if (releaseEffect >= 0) { - effects[releaseEffect].trigger(); - releaseEffect = -1; - } - } - - public void mouseWheel(int delta) { - if (mouseY > firstPatternY) { - int patternIndex = objectClickIndex(firstPatternY); - if (patternIndex < PATTERN_LIST_LENGTH) { - patternScrollPos = constrain(patternScrollPos + delta, 0, patterns.length - PATTERN_LIST_LENGTH); - } - } - } - -} - -/** - * UI for control of mapping. - */ -class MappingUI extends OverlayUI { - - private MappingTool mappingTool; - - private final String MAPPING_MODE_ALL = "All On"; - private final String MAPPING_MODE_CHANNEL = "Channel"; - private final String MAPPING_MODE_SINGLE_CUBE = "Single Cube"; - - private final String[] mappingModes = { - MAPPING_MODE_ALL, - MAPPING_MODE_CHANNEL, - MAPPING_MODE_SINGLE_CUBE - }; - private final Method mappingModeStateMethod; - - private final String CUBE_MODE_ALL = "All Strips"; - private final String CUBE_MODE_SINGLE_STRIP = "Single Strip"; - private final String CUBE_MODE_STRIP_PATTERN = "Strip Pattern"; - private final String[] cubeModes = { - CUBE_MODE_ALL, - CUBE_MODE_SINGLE_STRIP, - CUBE_MODE_STRIP_PATTERN - }; - private final Method cubeModeStateMethod; - - private final String CHANNEL_MODE_RED = "Red"; - private final String CHANNEL_MODE_GREEN = "Green"; - private final String CHANNEL_MODE_BLUE = "Blue"; - private final String[] channelModes = { - CHANNEL_MODE_RED, - CHANNEL_MODE_GREEN, - CHANNEL_MODE_BLUE, - }; - private final Method channelModeStateMethod; - - private int firstMappingY; - private int firstCubeY; - private int firstChannelY; - private int channelFieldY; - private int cubeFieldY; - private int stripFieldY; - - private boolean dragCube; - private boolean dragStrip; - private boolean dragChannel; - - MappingUI(MappingTool mappingTool) { - this.mappingTool = mappingTool; - try { - mappingModeStateMethod = getClass().getMethod("getMappingState", Object.class); - channelModeStateMethod = getClass().getMethod("getChannelState", Object.class); - cubeModeStateMethod = getClass().getMethod("getCubeState", Object.class); - } catch (Exception x) { - throw new RuntimeException(x); - } - } - - public int getMappingState(Object mappingMode) { - boolean active = false; - if (mappingMode == MAPPING_MODE_ALL) { - active = mappingTool.mappingMode == mappingTool.MAPPING_MODE_ALL; - } else if (mappingMode == MAPPING_MODE_CHANNEL) { - active = mappingTool.mappingMode == mappingTool.MAPPING_MODE_CHANNEL; - } else if (mappingMode == MAPPING_MODE_SINGLE_CUBE) { - active = mappingTool.mappingMode == mappingTool.MAPPING_MODE_SINGLE_CUBE; - } - return active ? STATE_ACTIVE : STATE_DEFAULT; - } - - public int getChannelState(Object channelMode) { - boolean active = false; - if (channelMode == CHANNEL_MODE_RED) { - active = mappingTool.channelModeRed; - } else if (channelMode == CHANNEL_MODE_GREEN) { - active = mappingTool.channelModeGreen; - } else if (channelMode == CHANNEL_MODE_BLUE) { - active = mappingTool.channelModeBlue; - } - return active ? STATE_ACTIVE : STATE_DEFAULT; - } - - public int getCubeState(Object cubeMode) { - boolean active = false; - if (cubeMode == CUBE_MODE_ALL) { - active = mappingTool.cubeMode == mappingTool.CUBE_MODE_ALL; - } else if (cubeMode == CUBE_MODE_SINGLE_STRIP) { - active = mappingTool.cubeMode == mappingTool.CUBE_MODE_SINGLE_STRIP; - } else if (cubeMode == CUBE_MODE_STRIP_PATTERN) { - active = mappingTool.cubeMode == mappingTool.CUBE_MODE_STRIP_PATTERN; - } - return active ? STATE_ACTIVE : STATE_DEFAULT; - } - - public void draw() { - drawLogoAndBackground(); - - int yPos = 0; - firstMappingY = yPos + lineHeight + 6; - yPos = drawObjectList(yPos, "MAPPING MODE", mappingModes, mappingModes, mappingModeStateMethod); - yPos += sectionSpacing; - - firstCubeY = yPos + lineHeight + 6; - yPos = drawObjectList(yPos, "CUBE MODE", cubeModes, cubeModes, cubeModeStateMethod); - yPos += sectionSpacing; - - firstChannelY = yPos + lineHeight + 6; - yPos = drawObjectList(yPos, "CHANNELS", channelModes, channelModes, channelModeStateMethod); - yPos += sectionSpacing; - - channelFieldY = yPos + lineHeight + 6; - yPos = drawValueField(yPos, "CHANNEL ID", mappingTool.channelIndex + 1); - yPos += sectionSpacing; - - cubeFieldY = yPos + lineHeight + 6; - yPos = drawValueField(yPos, "CUBE ID", glucose.model.getRawIndexForCube(mappingTool.cubeIndex)); - yPos += sectionSpacing; - - stripFieldY = yPos + lineHeight + 6; - yPos = drawValueField(yPos, "STRIP ID", mappingTool.stripIndex + 1); - - drawToggleTip("Tap 'm' to return"); - } - - private int drawValueField(int yPos, String label, int value) { - yPos += lineHeight; - textAlign(LEFT); - textFont(titleFont); - fill(titleColor); - text(label, leftTextPos, yPos); - fill(0); - yPos += 6; - rect(leftTextPos, yPos, w-8, lineHeight); - yPos += lineHeight; - - fill(#999999); - textAlign(CENTER); - textFont(itemFont); - text("" + value, leftTextPos + (w-8)/2, yPos - 5); - - return yPos; - } - - private int lastY; - - public void mousePressed() { - dragCube = dragStrip = dragChannel = false; - lastY = mouseY; - - if (mouseX < leftPos) { - return; - } - - if (mouseY >= stripFieldY) { - if (mouseY < stripFieldY + lineHeight) { - dragStrip = true; - } - } else if (mouseY >= cubeFieldY) { - if (mouseY < cubeFieldY + lineHeight) { - dragCube = true; - } - } else if (mouseY >= channelFieldY) { - if (mouseY < channelFieldY + lineHeight) { - dragChannel = true; - } - } else if (mouseY >= firstChannelY) { - int index = objectClickIndex(firstChannelY); - switch (index) { - case 0: mappingTool.channelModeRed = !mappingTool.channelModeRed; break; - case 1: mappingTool.channelModeGreen = !mappingTool.channelModeGreen; break; - case 2: mappingTool.channelModeBlue = !mappingTool.channelModeBlue; break; - } - } else if (mouseY >= firstCubeY) { - int index = objectClickIndex(firstCubeY); - switch (index) { - case 0: mappingTool.cubeMode = mappingTool.CUBE_MODE_ALL; break; - case 1: mappingTool.cubeMode = mappingTool.CUBE_MODE_SINGLE_STRIP; break; - case 2: mappingTool.cubeMode = mappingTool.CUBE_MODE_STRIP_PATTERN; break; - } - } else if (mouseY >= firstMappingY) { - int index = objectClickIndex(firstMappingY); - switch (index) { - case 0: mappingTool.mappingMode = mappingTool.MAPPING_MODE_ALL; break; - case 1: mappingTool.mappingMode = mappingTool.MAPPING_MODE_CHANNEL; break; - case 2: mappingTool.mappingMode = mappingTool.MAPPING_MODE_SINGLE_CUBE; break; - } - } - } - - public void mouseReleased() {} - public void mouseWheel(int delta) {} - - public void mouseDragged() { - final int DRAG_THRESHOLD = 5; - int dy = lastY - mouseY; - if (abs(dy) >= DRAG_THRESHOLD) { - lastY = mouseY; - if (dragCube) { - if (dy < 0) { - mappingTool.decCube(); - } else { - mappingTool.incCube(); - } - } else if (dragStrip) { - if (dy < 0) { - mappingTool.decStrip(); - } else { - mappingTool.incStrip(); - } - } else if (dragChannel) { - if (dy < 0) { - mappingTool.decChannel(); - } else { - mappingTool.incChannel(); - } - } - } - - } -} - -class DebugUI { - - final ChannelMapping[] channelList; - final int debugX = 5; - final int debugY = 5; - final int debugXSpacing = 28; - final int debugYSpacing = 21; - final int[][] debugState; - final int[] indexState; - - final int CUBE_STATE_UNUSED = 0; - final int CUBE_STATE_USED = 1; - final int CUBE_STATE_DUPLICATED = 2; - - final int DEBUG_STATE_ANIM = 0; - final int DEBUG_STATE_WHITE = 1; - final int DEBUG_STATE_OFF = 2; - final int DEBUG_STATE_UNUSED = 3; - - DebugUI(PandaMapping[] pandaMappings) { - int totalChannels = pandaMappings.length * PandaMapping.CHANNELS_PER_BOARD; - debugState = new int[totalChannels+1][ChannelMapping.CUBES_PER_CHANNEL+1]; - indexState = new int[glucose.model.cubes.size()+1]; - - channelList = new ChannelMapping[totalChannels]; - int channelIndex = 0; - for (PandaMapping pm : pandaMappings) { - for (ChannelMapping channel : pm.channelList) { - channelList[channelIndex++] = channel; - } - } - for (int i = 0; i < debugState.length; ++i) { - for (int j = 0; j < debugState[i].length; ++j) { - debugState[i][j] = DEBUG_STATE_ANIM; - } - } - - for (int rawIndex = 0; rawIndex < glucose.model.cubes.size()+1; ++rawIndex) { - indexState[rawIndex] = CUBE_STATE_UNUSED; - } - for (ChannelMapping channel : channelList) { - for (int rawCubeIndex : channel.objectIndices) { - if (rawCubeIndex > 0) - ++indexState[rawCubeIndex]; - } - } - } - - void draw() { - noStroke(); - int xBase = debugX; - int yPos = debugY; - - fill(#000000); - rect(0, 0, debugX + 5*debugXSpacing, height); - - int channelNum = 0; - for (ChannelMapping channel : channelList) { - int xPos = xBase; - drawNumBox(xPos, yPos, channelNum+1, debugState[channelNum][0]); - xPos += debugXSpacing; - - switch (channel.mode) { - case ChannelMapping.MODE_CUBES: - int stateIndex = 0; - boolean first = true; - for (int rawCubeIndex : channel.objectIndices) { - if (rawCubeIndex < 0) { - break; - } - if (first) { - first = false; - } else { - stroke(#999999); - line(xPos - 12, yPos + 8, xPos, yPos + 8); - } - drawNumBox(xPos, yPos, rawCubeIndex, debugState[channelNum][stateIndex+1], indexState[rawCubeIndex]); - ++stateIndex; - xPos += debugXSpacing; - } - break; - case ChannelMapping.MODE_BASS: - drawNumBox(xPos, yPos, "B", debugState[channelNum][1]); - break; - case ChannelMapping.MODE_SPEAKER: - drawNumBox(xPos, yPos, "S" + channel.objectIndices[0], debugState[channelNum][1]); - break; - case ChannelMapping.MODE_STRUTS_AND_FLOOR: - drawNumBox(xPos, yPos, "F", debugState[channelNum][1]); - break; - case ChannelMapping.MODE_NULL: - break; - default: - throw new RuntimeException("Unhandled channel mapping mode: " + channel.mode); - } - - yPos += debugYSpacing; - ++channelNum; - } - drawNumBox(xBase, yPos, "A", debugState[channelNum][0]); - yPos += debugYSpacing * 2; - - noFill(); - fill(#CCCCCC); - text("Unused Cubes", xBase, yPos + 12); - yPos += debugYSpacing; - - int xIndex = 0; - for (int rawIndex = 1; rawIndex <= glucose.model.cubes.size(); ++rawIndex) { - if (indexState[rawIndex] == CUBE_STATE_UNUSED) { - drawNumBox(xBase + (xIndex * debugXSpacing), yPos, rawIndex, DEBUG_STATE_UNUSED); - ++xIndex; - if (xIndex > 4) { - xIndex = 0; - yPos += debugYSpacing + 2; - } - } - } - } - - - void drawNumBox(int xPos, int yPos, int label, int state) { - drawNumBox(xPos, yPos, "" + label, state); - } - - void drawNumBox(int xPos, int yPos, String label, int state) { - drawNumBox(xPos, yPos, "" + label, state, CUBE_STATE_USED); - } - - void drawNumBox(int xPos, int yPos, int label, int state, int cubeState) { - drawNumBox(xPos, yPos, "" + label, state, cubeState); - } - - void drawNumBox(int xPos, int yPos, String label, int state, int cubeState) { - noFill(); - color textColor = #cccccc; - switch (state) { - case DEBUG_STATE_ANIM: - noStroke(); - fill(#880000); - rect(xPos, yPos, 16, 8); - fill(#000088); - rect(xPos, yPos+8, 16, 8); - noFill(); - stroke(textColor); - break; - case DEBUG_STATE_WHITE: - stroke(textColor); - fill(#e9e9e9); - textColor = #333333; - break; - case DEBUG_STATE_OFF: - stroke(textColor); - break; - case DEBUG_STATE_UNUSED: - stroke(textColor); - fill(#880000); - break; - } - - if (cubeState >= CUBE_STATE_DUPLICATED) { - stroke(textColor = #FF0000); - } - - rect(xPos, yPos, 16, 16); - noStroke(); - fill(textColor); - text(label, xPos + 2, yPos + 12); - } - - void maskColors(color[] colors) { - color white = #FFFFFF; - color off = #000000; - int channelIndex = 0; - int state; - for (ChannelMapping channel : channelList) { - switch (channel.mode) { - case ChannelMapping.MODE_CUBES: - int cubeIndex = 1; - for (int rawCubeIndex : channel.objectIndices) { - if (rawCubeIndex >= 0) { - state = debugState[channelIndex][cubeIndex]; - if (state != DEBUG_STATE_ANIM) { - color debugColor = (state == DEBUG_STATE_WHITE) ? white : off; - Cube cube = glucose.model.getCubeByRawIndex(rawCubeIndex); - for (Point p : cube.points) { - colors[p.index] = debugColor; - } - } - } - ++cubeIndex; - } - break; - - case ChannelMapping.MODE_BASS: - state = debugState[channelIndex][1]; - if (state != DEBUG_STATE_ANIM) { - color debugColor = (state == DEBUG_STATE_WHITE) ? white : off; - for (Strip s : glucose.model.bassBox.boxStrips) { - for (Point p : s.points) { - colors[p.index] = debugColor; - } - } - } - break; - - case ChannelMapping.MODE_STRUTS_AND_FLOOR: - state = debugState[channelIndex][1]; - if (state != DEBUG_STATE_ANIM) { - color debugColor = (state == DEBUG_STATE_WHITE) ? white : off; - for (Point p : glucose.model.boothFloor.points) { - colors[p.index] = debugColor; - } - for (Strip s : glucose.model.bassBox.struts) { - for (Point p : s.points) { - colors[p.index] = debugColor; - } - } - } - break; - - case ChannelMapping.MODE_SPEAKER: - state = debugState[channelIndex][1]; - if (state != DEBUG_STATE_ANIM) { - color debugColor = (state == DEBUG_STATE_WHITE) ? white : off; - for (Point p : glucose.model.speakers.get(channel.objectIndices[0]).points) { - colors[p.index] = debugColor; - } - } - break; - - case ChannelMapping.MODE_NULL: - break; - - default: - throw new RuntimeException("Unhandled channel mapping mode: " + channel.mode); - } - ++channelIndex; - } - } - - void mousePressed() { - int dx = (mouseX - debugX) / debugXSpacing; - int dy = (mouseY - debugY) / debugYSpacing; - if ((dy >= 0) && (dy < debugState.length)) { - if ((dx >= 0) && (dx < debugState[dy].length)) { - int newState = debugState[dy][dx] = (debugState[dy][dx] + 1) % 3; - if (dy == debugState.length-1) { - for (int[] states : debugState) { - for (int i = 0; i < states.length; ++i) { - states[i] = newState; - } - } - } else if (dx == 0) { - for (int i = 0; i < debugState[dy].length; ++i) { - debugState[dy][i] = newState; - } - } - } - } - } -} diff --git a/_PandaDriver.pde b/_PandaDriver.pde index a65a0fe..2d28cd6 100644 --- a/_PandaDriver.pde +++ b/_PandaDriver.pde @@ -15,6 +15,13 @@ import oscP5.*; * will be moved into GLucose once stabilized. */ public static class PandaDriver { + + interface Listener { + public void onToggle(boolean enabled); + } + + private Listener listener = null; + // IP address public final String ip; @@ -35,23 +42,6 @@ public static class PandaDriver { private static final int NO_POINT = -1; - public PandaDriver(String ip) { - this.ip = ip; - - // Initialize our OSC output stuff - address = new NetAddress(ip, 9001); - message = new OscMessage("/shady/pointbuffer"); - - // Build the array of points, initialize all to nothing - points = new int[PandaMapping.PIXELS_PER_BOARD]; - for (int i = 0; i < points.length; ++i) { - points[i] = NO_POINT; - } - } - - private final static int FORWARD = -1; - private final static int BACKWARD = -2; - //////////////////////////////////////////////////////////////// // // READ THIS RIGHT NOW BEFORE YOU MODIFY THE BELOW!!!!!!!!!!!!! @@ -85,6 +75,8 @@ public static class PandaDriver { // //////////////////////////////////////////////////////////////// + private final static int FORWARD = -1; + private final static int BACKWARD = -2; /** * These constant arrays indicate the order in which the strips of a cube @@ -164,7 +156,21 @@ public static class PandaDriver { {3, BACKWARD }, } }; - + + public PandaDriver(String ip) { + this.ip = ip; + + // Initialize our OSC output stuff + address = new NetAddress(ip, 9001); + message = new OscMessage("/shady/pointbuffer"); + + // Build the array of points, initialize all to nothing + points = new int[PandaMapping.PIXELS_PER_BOARD]; + for (int i = 0; i < points.length; ++i) { + points[i] = NO_POINT; + } + } + public PandaDriver(String ip, Model model, PandaMapping pm) { this(ip); @@ -271,23 +277,31 @@ public static class PandaDriver { return pi; } - public void disable() { - if (enabled) { - enabled = false; - println("PandaBoard/" + ip + ": OFF"); + public PandaDriver setListener(Listener listener) { + this.listener = listener; + return this; + } + + public void setEnabled(boolean enabled) { + if (this.enabled != enabled) { + this.enabled = enabled; + println("PandaBoard/" + ip + ": " + (enabled ? "ON" : "OFF")); + if (listener != null) { + listener.onToggle(enabled); + } } } + + public void disable() { + setEnabled(false); + } public void enable() { - if (!enabled) { - enabled = true; - println("PandaBoard/" + ip + ": ON"); - } + setEnabled(true); } public void toggle() { - enabled = !enabled; - println("PandaBoard/" + ip + ": " + (enabled ? "ON" : "OFF")); + setEnabled(!enabled); } public final void send(int[] colors) { diff --git a/_UIFramework.pde b/_UIFramework.pde new file mode 100644 index 0000000..a2946fb --- /dev/null +++ b/_UIFramework.pde @@ -0,0 +1,825 @@ +/** + * DOUBLE BLACK DIAMOND DOUBLE BLACK DIAMOND + * + * //\\ //\\ //\\ //\\ + * ///\\\ ///\\\ ///\\\ ///\\\ + * \\\/// \\\/// \\\/// \\\/// + * \\// \\// \\// \\// + * + * EXPERTS ONLY!! EXPERTS ONLY!! + * + * Little UI framework in progress to handle mouse events, layout, + * redrawing, etc. + */ + +final color lightGreen = #669966; +final color lightBlue = #666699; +final color bgGray = #444444; +final color defaultTextColor = #999999; +final PFont defaultItemFont = createFont("Lucida Grande", 11); +final PFont defaultTitleFont = createFont("Myriad Pro", 10); + +public abstract class UIObject { + + protected final List children = new ArrayList(); + + protected boolean needsRedraw = true; + protected boolean childNeedsRedraw = true; + + protected float x=0, y=0, w=0, h=0; + + public UIContainer parent = null; + + protected boolean visible = true; + + public UIObject() {} + + public UIObject(float x, float y, float w, float h) { + this.x = x; + this.y = y; + this.w = w; + this.h = h; + } + + public boolean isVisible() { + return visible; + } + + public UIObject setVisible(boolean visible) { + if (visible != this.visible) { + this.visible = visible; + redraw(); + } + return this; + } + + public final UIObject setPosition(float x, float y) { + this.x = x; + this.y = y; + redraw(); + return this; + } + + public final UIObject setSize(float w, float h) { + this.w = w; + this.h = h; + redraw(); + return this; + } + + public final UIObject addToContainer(UIContainer c) { + c.children.add(this); + this.parent = c; + return this; + } + + public final UIObject removeFromContainer(UIContainer c) { + c.children.remove(this); + this.parent = null; + return this; + } + + public final UIObject redraw() { + _redraw(); + UIObject p = this.parent; + while (p != null) { + p.childNeedsRedraw = true; + p = p.parent; + } + return this; + } + + private final void _redraw() { + needsRedraw = true; + for (UIObject child : children) { + childNeedsRedraw = true; + child._redraw(); + } + } + + public final void draw(PGraphics pg) { + if (!visible) { + return; + } + if (needsRedraw) { + needsRedraw = false; + onDraw(pg); + } + if (childNeedsRedraw) { + childNeedsRedraw = false; + for (UIObject child : children) { + if (needsRedraw || child.needsRedraw || child.childNeedsRedraw) { + pg.pushMatrix(); + pg.translate(child.x, child.y); + child.draw(pg); + pg.popMatrix(); + } + } + } + } + + public final boolean contains(float x, float y) { + return + (x >= this.x && x < (this.x + this.w)) && + (y >= this.y && y < (this.y + this.h)); + } + + protected void onDraw(PGraphics pg) {} + protected void onMousePressed(float mx, float my) {} + protected void onMouseReleased(float mx, float my) {} + protected void onMouseDragged(float mx, float my, float dx, float dy) {} + protected void onMouseWheel(float mx, float my, float dx) {} +} + +public class UIContainer extends UIObject { + + private UIObject focusedChild = null; + + public UIContainer() {} + + public UIContainer(float x, float y, float w, float h) { + super(x, y, w, h); + } + + public UIContainer(UIObject[] children) { + for (UIObject child : children) { + child.addToContainer(this); + } + } + + protected void onMousePressed(float mx, float my) { + for (int i = children.size() - 1; i >= 0; --i) { + UIObject child = children.get(i); + if (child.contains(mx, my)) { + child.onMousePressed(mx - child.x, my - child.y); + focusedChild = child; + break; + } + } + } + + protected void onMouseReleased(float mx, float my) { + if (focusedChild != null) { + focusedChild.onMouseReleased(mx - focusedChild.x, my - focusedChild.y); + } + focusedChild = null; + } + + protected void onMouseDragged(float mx, float my, float dx, float dy) { + if (focusedChild != null) { + focusedChild.onMouseDragged(mx - focusedChild.x, my - focusedChild.y, dx, dy); + } + } + + protected void onMouseWheel(float mx, float my, float delta) { + for (UIObject child : children) { + if (child.contains(mx, my)) { + child.onMouseWheel(mx - child.x, mx - child.y, delta); + } + } + } + +} + +public class UIContext extends UIContainer { + + final public PGraphics pg; + + UIContext(float x, float y, float w, float h) { + super(x, y, w, h); + pg = createGraphics((int)w, (int)h, JAVA2D); + pg.smooth(); + } + + public void draw() { + if (!visible) { + return; + } + if (needsRedraw || childNeedsRedraw) { + pg.beginDraw(); + draw(pg); + pg.endDraw(); + } + image(pg, x, y); + } + + private float px, py; + private boolean dragging = false; + + public boolean mousePressed(float mx, float my) { + if (!visible) { + return false; + } + if (contains(mx, my)) { + dragging = true; + px = mx; + py = my; + onMousePressed(mx - x, my - y); + return true; + } + return false; + } + + public boolean mouseReleased(float mx, float my) { + if (!visible) { + return false; + } + dragging = false; + onMouseReleased(mx - x, my - y); + return true; + } + + public boolean mouseDragged(float mx, float my) { + if (!visible) { + return false; + } + if (dragging) { + float dx = mx - px; + float dy = my - py; + onMouseDragged(mx - x, my - y, dx, dy); + px = mx; + py = my; + return true; + } + return false; + } + + public boolean mouseWheel(float mx, float my, float delta) { + if (!visible) { + return false; + } + if (contains(mx, my)) { + onMouseWheel(mx - x, my - y, delta); + return true; + } + return false; + } +} + +public class UIWindow extends UIContext { + + protected final static int titleHeight = 24; + + public UIWindow(String label, float x, float y, float w, float h) { + super(x, y, w, h); + new UILabel(6, 8, w-6, titleHeight-8) { + protected void onMouseDragged(float mx, float my, float dx, float dy) { + parent.x = constrain(parent.x + dx, 0, width - w); + parent.y = constrain(parent.y + dy, 0, height - h); + } + }.setLabel(label).setFont(defaultTitleFont).addToContainer(this); + } + + protected void onDraw(PGraphics pg) { + pg.noStroke(); + pg.fill(#444444); + pg.stroke(#292929); + pg.rect(0, 0, w-1, h-1); + } +} + +public class UILabel extends UIObject { + + private PFont font = defaultTitleFont; + private color fontColor = #CCCCCC; + private String label = ""; + + public UILabel(float x, float y, float w, float h) { + super(x, y, w, h); + } + + protected void onDraw(PGraphics pg) { + pg.textAlign(LEFT, TOP); + pg.textFont(font); + pg.fill(fontColor); + pg.text(label, 0, 0); + } + + public UILabel setFont(PFont font) { + this.font = font; + redraw(); + return this; + } + + public UILabel setFontColor(color fontColor) { + this.fontColor = fontColor; + redraw(); + return this; + } + + public UILabel setLabel(String label) { + this.label = label; + redraw(); + return this; + } +} + +public class UIButton extends UIObject { + + private boolean active = false; + private boolean isMomentary = false; + private color borderColor = #666666; + private color inactiveColor = #222222; + private color activeColor = #669966; + private color labelColor = #999999; + private String label = ""; + + public UIButton(float x, float y, float w, float h) { + super(x, y, w, h); + } + + public UIButton setMomentary(boolean momentary) { + isMomentary = momentary; + return this; + } + + protected void onDraw(PGraphics pg) { + pg.stroke(borderColor); + pg.fill(active ? activeColor : inactiveColor); + pg.rect(0, 0, w, h); + if (label != null && label.length() > 0) { + pg.fill(active ? #FFFFFF : labelColor); + pg.textFont(defaultItemFont); + pg.textAlign(CENTER); + pg.text(label, w/2, h-5); + } + } + + protected void onMousePressed(float mx, float my) { + if (isMomentary) { + setActive(true); + } else { + setActive(!active); + } + } + + protected void onMouseReleased(float mx, float my) { + if (isMomentary) { + setActive(false); + } + } + + public UIButton setActive(boolean active) { + this.active = active; + onToggle(active); + redraw(); + return this; + } + + public UIButton toggle() { + return setActive(!active); + } + + protected void onToggle(boolean active) {} + + public UIButton setBorderColor(color borderColor) { + if (this.borderColor != borderColor) { + this.borderColor = borderColor; + redraw(); + } + return this; + } + + public UIButton setActiveColor(color activeColor) { + if (this.activeColor != activeColor) { + this.activeColor = activeColor; + if (active) { + redraw(); + } + } + return this; + } + + public UIButton setInactiveColor(color inactiveColor) { + if (this.inactiveColor != inactiveColor) { + this.inactiveColor = inactiveColor; + if (!active) { + redraw(); + } + } + return this; + } + + public UIButton setLabelColor(color labelColor) { + if (this.labelColor != labelColor) { + this.labelColor = labelColor; + redraw(); + } + return this; + } + + public UIButton setLabel(String label) { + if (!this.label.equals(label)) { + this.label = label; + redraw(); + } + return this; + } + + public void onMousePressed() { + setActive(!active); + } +} + +public class UIToggleSet extends UIObject { + + private String[] options; + private int[] boundaries; + private String value; + + public UIToggleSet(float x, float y, float w, float h) { + super(x, y, w, h); + } + + public UIToggleSet setOptions(String[] options) { + this.options = options; + boundaries = new int[options.length]; + int totalLength = 0; + for (String s : options) { + totalLength += s.length(); + } + int lengthSoFar = 0; + for (int i = 0; i < options.length; ++i) { + lengthSoFar += options[i].length(); + boundaries[i] = (int) (lengthSoFar * w / totalLength); + } + value = options[0]; + redraw(); + return this; + } + + public String getValue() { + return value; + } + + public UIToggleSet setValue(String option) { + value = option; + onToggle(value); + redraw(); + return this; + } + + public void onDraw(PGraphics pg) { + pg.stroke(#666666); + pg.fill(#222222); + pg.rect(0, 0, w, h); + for (int b : boundaries) { + pg.line(b, 1, b, h-1); + } + pg.noStroke(); + pg.textAlign(CENTER); + pg.textFont(defaultItemFont); + int leftBoundary = 0; + + for (int i = 0; i < options.length; ++i) { + boolean isActive = options[i] == value; + if (isActive) { + pg.fill(lightGreen); + pg.rect(leftBoundary + 1, 1, boundaries[i] - leftBoundary - 1, h-1); + } + pg.fill(isActive ? #FFFFFF : #999999); + pg.text(options[i], (leftBoundary + boundaries[i]) / 2., h-6); + leftBoundary = boundaries[i]; + } + } + + public void onMousePressed(float mx, float my) { + for (int i = 0; i < boundaries.length; ++i) { + if (mx < boundaries[i]) { + setValue(options[i]); + break; + } + } + } + + protected void onToggle(String option) {} + +} + + +public abstract class UIParameterControl extends UIObject implements LXParameter.Listener { + protected LXParameter parameter = null; + + protected UIParameterControl(float x, float y, float w, float h) { + super(x, y, w, h); + } + + public void onParameterChanged(LXParameter parameter) { + redraw(); + } + + public UIParameterControl setParameter(LXParameter parameter) { + if (this.parameter != null) { + if (this.parameter instanceof LXListenableParameter) { + ((LXListenableParameter)this.parameter).removeListener(this); + } + } + this.parameter = parameter; + if (this.parameter != null) { + if (this.parameter instanceof LXListenableParameter) { + ((LXListenableParameter)this.parameter).addListener(this); + } + } + redraw(); + return this; + } +} + +public class UIParameterKnob extends UIParameterControl { + private int knobSize = 28; + private final float knobIndent = .4; + private final int knobLabelHeight = 14; + + public UIParameterKnob(float x, float y) { + this(x, y, 0, 0); + setSize(knobSize, knobSize + knobLabelHeight); + } + + public UIParameterKnob(float x, float y, float w, float h) { + super(x, y, w, h); + } + + protected void onDraw(PGraphics pg) { + float knobValue = (parameter != null) ? parameter.getValuef() : 0; + + pg.ellipseMode(CENTER); + pg.noStroke(); + + pg.fill(bgGray); + pg.rect(0, 0, knobSize, knobSize); + + // Full outer dark ring + pg.fill(#222222); + pg.arc(knobSize/2, knobSize/2, knobSize, knobSize, HALF_PI + knobIndent, HALF_PI + knobIndent + (TWO_PI-2*knobIndent)); + + // Light ring indicating value + pg.fill(lightGreen); + pg.arc(knobSize/2, knobSize/2, knobSize, knobSize, HALF_PI + knobIndent, HALF_PI + knobIndent + knobValue*(TWO_PI-2*knobIndent)); + + // Center circle of knob + pg.fill(#333333); + pg.ellipse(knobSize/2, knobSize/2, knobSize/2, knobSize/2); + + String knobLabel = (parameter != null) ? parameter.getLabel() : null; + if (knobLabel == null) { + knobLabel = "-"; + } else if (knobLabel.length() > 4) { + knobLabel = knobLabel.substring(0, 4); + } + pg.fill(#000000); + pg.rect(0, knobSize + 2, knobSize, knobLabelHeight - 2); + pg.fill(#999999); + pg.textAlign(CENTER); + pg.textFont(defaultTitleFont); + pg.text(knobLabel, knobSize/2, knobSize + knobLabelHeight - 2); + } + + public void onMouseDragged(float mx, float my, float dx, float dy) { + if (parameter != null) { + float value = constrain(parameter.getValuef() - dy / 100., 0, 1); + parameter.setValue(value); + } + } +} + +public class UIParameterSlider extends UIParameterControl { + + private static final float handleWidth = 12; + + UIParameterSlider(float x, float y, float w, float h) { + super(x, y, w, h); + } + + protected void onDraw(PGraphics pg) { + pg.noStroke(); + pg.fill(#333333); + pg.rect(0, 0, w, h); + pg.fill(#222222); + pg.rect(4, h/2-2, w-8, 4); + pg.fill(#666666); + pg.stroke(#222222); + pg.rect((int) (4 + parameter.getValuef() * (w-8-handleWidth)), 4, handleWidth, h-8); + } + + private boolean editing = false; + protected void onMousePressed(float mx, float my) { + float handleLeft = 4 + parameter.getValuef() * (w-8-handleWidth); + if (mx >= handleLeft && mx < handleLeft + handleWidth) { + editing = true; + } + } + + protected void onMouseReleased(float mx, float my) { + editing = false; + } + + protected void onMouseDragged(float mx, float my, float dx, float dy) { + if (editing) { + parameter.setValue(constrain((mx - handleWidth/2. - 4) / (w-8-handleWidth), 0, 1)); + } + } +} + +public class UIScrollList extends UIObject { + + private List items = new ArrayList(); + + private PFont itemFont = defaultItemFont; + private int itemHeight = 20; + private color selectedColor = lightGreen; + private color pendingColor = lightBlue; + private int scrollOffset = 0; + private int numVisibleItems = 0; + + private boolean hasScroll; + private float scrollYStart; + private float scrollYHeight; + + public UIScrollList(float x, float y, float w, float h) { + super(x, y, w, h); + } + + protected void onDraw(PGraphics pg) { + int yp = 0; + boolean even = true; + for (int i = 0; i < numVisibleItems; ++i) { + if (i + scrollOffset >= items.size()) { + break; + } + ScrollItem item = items.get(i + scrollOffset); + color itemColor; + color labelColor = #FFFFFF; + if (item.isSelected()) { + itemColor = selectedColor; + } else if (item.isPending()) { + itemColor = pendingColor; + } else { + labelColor = #000000; + itemColor = even ? #666666 : #777777; + } + pg.noStroke(); + pg.fill(itemColor); + pg.rect(0, yp, w, itemHeight); + pg.fill(labelColor); + pg.textFont(itemFont); + pg.textAlign(LEFT, TOP); + pg.text(item.getLabel(), 6, yp+4); + + yp += itemHeight; + even = !even; + } + if (hasScroll) { + pg.noStroke(); + pg.fill(color(0, 0, 100, 15)); + pg.rect(w-12, 0, 12, h); + pg.fill(#333333); + pg.rect(w-12, scrollYStart, 12, scrollYHeight); + } + + } + + private boolean scrolling = false; + private ScrollItem pressedItem = null; + + public void onMousePressed(float mx, float my) { + pressedItem = null; + if (hasScroll && mx >= w-12) { + if (my >= scrollYStart && my < (scrollYStart + scrollYHeight)) { + scrolling = true; + dAccum = 0; + } + } else { + int index = (int) my / itemHeight; + if (scrollOffset + index < items.size()) { + pressedItem = items.get(scrollOffset + index); + pressedItem.onMousePressed(); + pressedItem.select(); + redraw(); + } + } + } + + public void onMouseReleased(float mx, float my) { + scrolling = false; + if (pressedItem != null) { + pressedItem.onMouseReleased(); + redraw(); + } + } + + private float dAccum = 0; + public void onMouseDragged(float mx, float my, float dx, float dy) { + if (scrolling) { + dAccum += dy; + float scrollOne = h / items.size(); + int offset = (int) (dAccum / scrollOne); + if (offset != 0) { + dAccum -= offset * scrollOne; + setScrollOffset(scrollOffset + offset); + } + } + } + + private float wAccum = 0; + public void onMouseWheel(float mx, float my, float delta) { + wAccum += delta; + int offset = (int) (wAccum / 5); + if (offset != 0) { + wAccum -= offset * 5; + setScrollOffset(scrollOffset + offset); + } + } + + public void setScrollOffset(int offset) { + scrollOffset = constrain(offset, 0, items.size() - numVisibleItems); + scrollYStart = (int) (scrollOffset * h / items.size()); + scrollYHeight = (int) (numVisibleItems * h / (float) items.size()); + redraw(); + } + + public UIScrollList setItems(List items) { + this.items = items; + numVisibleItems = (int) (h / itemHeight); + hasScroll = items.size() > numVisibleItems; + setScrollOffset(0); + redraw(); + return this; + } +} + +public interface ScrollItem { + public boolean isSelected(); + public boolean isPending(); + public String getLabel(); + public void select(); + public void onMousePressed(); + public void onMouseReleased(); +} + +public abstract class AbstractScrollItem implements ScrollItem { + public boolean isPending() { + return false; + } + public void select() {} + public void onMousePressed() {} + public void onMouseReleased() {} +} + +public class UIIntegerBox extends UIObject { + + private int minValue = 0; + private int maxValue = MAX_INT; + private int value = 0; + + UIIntegerBox(float x, float y, float w, float h) { + super(x, y, w, h); + } + + public UIIntegerBox setRange(int minValue, int maxValue) { + this.minValue = minValue; + this.maxValue = maxValue; + setValue(constrain(value, minValue, maxValue)); + return this; + } + + protected void onDraw(PGraphics pg) { + pg.stroke(#999999); + pg.fill(#222222); + pg.rect(0, 0, w, h); + pg.textAlign(CENTER, CENTER); + pg.textFont(defaultItemFont); + pg.fill(#999999); + pg.text("" + value, w/2, h/2); + } + + protected void onValueChange(int value) {} + + float dAccum = 0; + protected void onMousePressed(float mx, float my) { + dAccum = 0; + } + + protected void onMouseDragged(float mx, float my, float dx, float dy) { + dAccum -= dy; + int offset = (int) (dAccum / 5); + dAccum = dAccum - (offset * 5); + setValue(value + offset); + } + + public int getValue() { + return value; + } + + public UIIntegerBox setValue(int value) { + if (this.value != value) { + int range = (maxValue - minValue + 1); + while (value < minValue) { + value += range; + } + this.value = minValue + (value - minValue) % range; + this.onValueChange(this.value); + redraw(); + } + return this; + } +} diff --git a/_UIImplementation.pde b/_UIImplementation.pde new file mode 100644 index 0000000..c8b9930 --- /dev/null +++ b/_UIImplementation.pde @@ -0,0 +1,450 @@ +/** + * DOUBLE BLACK DIAMOND DOUBLE BLACK DIAMOND + * + * //\\ //\\ //\\ //\\ + * ///\\\ ///\\\ ///\\\ ///\\\ + * \\\/// \\\/// \\\/// \\\/// + * \\// \\// \\// \\// + * + * EXPERTS ONLY!! EXPERTS ONLY!! + * + * Custom UI components using the framework. + */ + +class UIPatternDeck extends UIWindow { + + Engine.Deck deck; + + public UIPatternDeck(Engine.Deck deck, String label, float x, float y, float w, float h) { + super(label, x, y, w, h); + this.deck = deck; + int yp = titleHeight; + + List items = new ArrayList(); + for (LXPattern p : deck.getPatterns()) { + items.add(new PatternScrollItem(p)); + } + final UIScrollList patternList = new UIScrollList(1, yp, w-2, 160).setItems(items); + patternList.addToContainer(this); + yp += patternList.h + 10; + + final UIParameterKnob[] parameterKnobs = new UIParameterKnob[12]; + for (int ki = 0; ki < parameterKnobs.length; ++ki) { + parameterKnobs[ki] = new UIParameterKnob(5 + 34*(ki % 4), yp + (ki/4) * 48); + parameterKnobs[ki].addToContainer(this); + } + + Engine.Listener lxListener = new Engine.Listener() { + public void patternWillChange(Engine.Deck deck, LXPattern pattern, LXPattern nextPattern) { + patternList.redraw(); + } + public void patternDidChange(Engine.Deck deck, LXPattern pattern) { + patternList.redraw(); + int pi = 0; + for (LXParameter parameter : pattern.getParameters()) { + if (pi >= parameterKnobs.length) { + break; + } + parameterKnobs[pi++].setParameter(parameter); + } + while (pi < parameterKnobs.length) { + parameterKnobs[pi++].setParameter(null); + } + } + }; + + deck.addListener(lxListener); + lxListener.patternDidChange(deck, deck.getActivePattern()); + + } + + class PatternScrollItem extends AbstractScrollItem { + + private LXPattern pattern; + private String label; + + PatternScrollItem(LXPattern pattern) { + this.pattern = pattern; + label = className(pattern, "Pattern"); + } + + public String getLabel() { + return label; + } + + public boolean isSelected() { + return deck.getActivePattern() == pattern; + } + + public boolean isPending() { + return deck.getNextPattern() == pattern; + } + + public void select() { + deck.goPattern(pattern); + } + } +} + +class UICrossfader extends UIWindow { + + public UICrossfader(float x, float y, float w, float h) { + super("CROSSFADER", x, y, w, h); + + List items = new ArrayList(); + for (LXTransition t : transitions) { + items.add(new TransitionScrollItem(t)); + } + new UIScrollList(1, titleHeight, w-2, 60).setItems(items).addToContainer(this); + new UIParameterSlider(6, titleHeight + 66, w-12, 24).setParameter(lx.engine.getDeck(1).getCrossfader()).addToContainer(this); + new UIToggleSet(6, 122, w-12, 20) { + protected void onToggle(String value) { + displayMode = value; + } + }.setOptions(new String[] { "A", "COMP", "B" }).setValue(displayMode = "COMP").addToContainer(this); + } +} + +class TransitionScrollItem extends AbstractScrollItem { + private final LXTransition transition; + private String label; + + TransitionScrollItem(LXTransition transition) { + this.transition = transition; + label = className(transition, "Transition"); + } + + public String getLabel() { + return label; + } + + public boolean isSelected() { + return transition == lx.engine.getDeck(1).getBlendTransition(); + } + + public boolean isPending() { + return false; + } + + public void select() { + lx.engine.getDeck(1).setBlendTransition(transition); + } +} + +class UIEffects extends UIWindow { + UIEffects(float x, float y, float w, float h) { + super("FX", x, y, w, h); + + int yp = titleHeight; + List items = new ArrayList(); + for (LXEffect fx : glucose.lx.getEffects()) { + items.add(new FXScrollItem(fx)); + } + final UIScrollList effectsList = new UIScrollList(1, yp, w-2, 60).setItems(items); + effectsList.addToContainer(this); + yp += effectsList.h + 10; + + final UIParameterKnob[] parameterKnobs = new UIParameterKnob[4]; + for (int ki = 0; ki < parameterKnobs.length; ++ki) { + parameterKnobs[ki] = new UIParameterKnob(5 + 34*(ki % 4), yp + (ki/4) * 48); + parameterKnobs[ki].addToContainer(this); + } + + GLucose.EffectListener fxListener = new GLucose.EffectListener() { + public void effectSelected(LXEffect effect) { + int i = 0; + for (LXParameter p : effect.getParameters()) { + if (i >= parameterKnobs.length) { + break; + } + parameterKnobs[i++].setParameter(p); + } + while (i < parameterKnobs.length) { + parameterKnobs[i++].setParameter(null); + } + } + }; + + glucose.addEffectListener(fxListener); + fxListener.effectSelected(glucose.getSelectedEffect()); + + } + + class FXScrollItem extends AbstractScrollItem { + + private LXEffect effect; + private String label; + + FXScrollItem(LXEffect effect) { + this.effect = effect; + label = className(effect, "Effect"); + } + + public String getLabel() { + return label; + } + + public boolean isSelected() { + return !effect.isEnabled() && (glucose.getSelectedEffect() == effect); + } + + public boolean isPending() { + return effect.isEnabled(); + } + + public void select() { + glucose.setSelectedEffect(effect); + } + + public void onMousePressed() { + if (glucose.getSelectedEffect() == effect) { + if (effect.isMomentary()) { + effect.enable(); + } else { + effect.toggle(); + } + } + } + + public void onMouseReleased() { + if (effect.isMomentary()) { + effect.disable(); + } + } + + } + +} + +class UIOutput extends UIWindow { + public UIOutput(float x, float y, float w, float h) { + super("OUTPUT", x, y, w, h); + float yp = titleHeight; + for (final PandaDriver panda : pandaBoards) { + final UIButton button = new UIButton(4, yp, w-8, 20) { + protected void onToggle(boolean active) { + panda.setEnabled(active); + } + }.setLabel(panda.ip); + button.addToContainer(this); + panda.setListener(new PandaDriver.Listener() { + public void onToggle(boolean active) { + button.setActive(active); + } + }); + yp += 24; + } + } +} + +class UITempo extends UIWindow { + + private final UIButton tempoButton; + + UITempo(float x, float y, float w, float h) { + super("TEMPO", x, y, w, h); + tempoButton = new UIButton(4, titleHeight, w-8, 20) { + protected void onToggle(boolean active) { + if (active) { + lx.tempo.tap(); + } + } + }.setMomentary(true); + tempoButton.addToContainer(this); + } + + public void draw() { + tempoButton.setLabel("" + ((int)(lx.tempo.bpm() * 10)) / 10.); + super.draw(); + + // Overlay tempo thing with openGL, redraw faster than button UI + fill(color(0, 0, 24 - 8*lx.tempo.rampf())); + noStroke(); + rect(x + 8, y + titleHeight + 5, 12, 12); + } +} + +class UIMapping extends UIWindow { + + private static final String MAP_MODE_ALL = "ALL"; + private static final String MAP_MODE_CHANNEL = "CHNL"; + private static final String MAP_MODE_CUBE = "CUBE"; + + private static final String CUBE_MODE_ALL = "ALL"; + private static final String CUBE_MODE_STRIP = "SNGL"; + private static final String CUBE_MODE_PATTERN = "PTRN"; + + private final MappingTool mappingTool; + + private final UIIntegerBox channelBox; + private final UIIntegerBox cubeBox; + private final UIIntegerBox stripBox; + + UIMapping(MappingTool tool, float x, float y, float w, float h) { + super("MAPPING", x, y, w, h); + mappingTool = tool; + + int yp = titleHeight; + new UIToggleSet(4, yp, w-8, 20) { + protected void onToggle(String value) { + if (value == MAP_MODE_ALL) mappingTool.mappingMode = mappingTool.MAPPING_MODE_ALL; + else if (value == MAP_MODE_CHANNEL) mappingTool.mappingMode = mappingTool.MAPPING_MODE_CHANNEL; + else if (value == MAP_MODE_CUBE) mappingTool.mappingMode = mappingTool.MAPPING_MODE_SINGLE_CUBE; + } + }.setOptions(new String[] { MAP_MODE_ALL, MAP_MODE_CHANNEL, MAP_MODE_CUBE }).addToContainer(this); + yp += 24; + new UILabel(4, yp+8, w-8, 20).setLabel("CHANNEL ID").addToContainer(this); + yp += 24; + (channelBox = new UIIntegerBox(4, yp, w-8, 20) { + protected void onValueChange(int value) { + mappingTool.setChannel(value-1); + } + }).setRange(1, mappingTool.numChannels()).addToContainer(this); + yp += 24; + + new UILabel(4, yp+8, w-8, 20).setLabel("CUBE ID").addToContainer(this); + yp += 24; + (cubeBox = new UIIntegerBox(4, yp, w-8, 20) { + protected void onValueChange(int value) { + mappingTool.setCube(value-1); + } + }).setRange(1, glucose.model.cubes.size()).addToContainer(this); + yp += 24; + + new UILabel(4, yp+8, w-8, 20).setLabel("COLORS").addToContainer(this); + yp += 24; + + new UIScrollList(1, yp, w-2, 60).setItems(Arrays.asList(new ScrollItem[] { + new ColorScrollItem(ColorScrollItem.COLOR_RED), + new ColorScrollItem(ColorScrollItem.COLOR_GREEN), + new ColorScrollItem(ColorScrollItem.COLOR_BLUE), + })).addToContainer(this); + yp += 64; + + new UILabel(4, yp+8, w-8, 20).setLabel("STRIP MODE").addToContainer(this); + yp += 24; + + new UIToggleSet(4, yp, w-8, 20) { + protected void onToggle(String value) { + if (value == CUBE_MODE_ALL) mappingTool.cubeMode = mappingTool.CUBE_MODE_ALL; + else if (value == CUBE_MODE_STRIP) mappingTool.cubeMode = mappingTool.CUBE_MODE_SINGLE_STRIP; + else if (value == CUBE_MODE_PATTERN) mappingTool.cubeMode = mappingTool.CUBE_MODE_STRIP_PATTERN; + } + }.setOptions(new String[] { CUBE_MODE_ALL, CUBE_MODE_STRIP, CUBE_MODE_PATTERN }).addToContainer(this); + + yp += 24; + new UILabel(4, yp+8, w-8, 20).setLabel("STRIP ID").addToContainer(this); + + yp += 24; + (stripBox = new UIIntegerBox(4, yp, w-8, 20) { + protected void onValueChange(int value) { + mappingTool.setStrip(value-1); + } + }).setRange(1, Cube.STRIPS_PER_CUBE).addToContainer(this); + + } + + public void setChannelID(int value) { + channelBox.setValue(value); + } + + public void setCubeID(int value) { + cubeBox.setValue(value); + } + + public void setStripID(int value) { + stripBox.setValue(value); + } + + class ColorScrollItem extends AbstractScrollItem { + + public static final int COLOR_RED = 1; + public static final int COLOR_GREEN = 2; + public static final int COLOR_BLUE = 3; + + private final int colorChannel; + + ColorScrollItem(int colorChannel) { + this.colorChannel = colorChannel; + } + + public String getLabel() { + switch (colorChannel) { + case COLOR_RED: return "Red"; + case COLOR_GREEN: return "Green"; + case COLOR_BLUE: return "Blue"; + } + return ""; + } + + public boolean isSelected() { + switch (colorChannel) { + case COLOR_RED: return mappingTool.channelModeRed; + case COLOR_GREEN: return mappingTool.channelModeGreen; + case COLOR_BLUE: return mappingTool.channelModeBlue; + } + return false; + } + + public void select() { + switch (colorChannel) { + case COLOR_RED: mappingTool.channelModeRed = !mappingTool.channelModeRed; break; + case COLOR_GREEN: mappingTool.channelModeGreen = !mappingTool.channelModeGreen; break; + case COLOR_BLUE: mappingTool.channelModeBlue = !mappingTool.channelModeBlue; break; + } + } + } +} + +class UIDebugText extends UIContext { + + private String line1 = ""; + private String line2 = ""; + + UIDebugText(float x, float y, float w, float h) { + super(x, y, w, h); + } + + public UIDebugText setText(String line1) { + return setText(line1, ""); + } + + public UIDebugText setText(String line1, String line2) { + if (!line1.equals(this.line1) || !line2.equals(this.line2)) { + this.line1 = line1; + this.line2 = line2; + setVisible(line1.length() + line2.length() > 0); + redraw(); + } + return this; + } + + protected void onDraw(PGraphics pg) { + super.onDraw(pg); + if (line1.length() + line2.length() > 0) { + pg.noStroke(); + pg.fill(#444444); + pg.rect(0, 0, w, h); + pg.textFont(defaultItemFont); + pg.textAlign(LEFT, TOP); + pg.fill(#cccccc); + pg.text(line1, 4, 4); + pg.text(line2, 4, 24); + } + } +} + +String className(Object p, String suffix) { + String s = p.getClass().getName(); + int li; + if ((li = s.lastIndexOf(".")) > 0) { + s = s.substring(li + 1); + } + if (s.indexOf("SugarCubes$") == 0) { + s = s.substring("SugarCubes$".length()); + } + if ((suffix != null) && ((li = s.indexOf(suffix)) != -1)) { + s = s.substring(0, li); + } + return s; +} diff --git a/code/GLucose.jar b/code/GLucose.jar index c36c6e992b80915841af721effbb5d42337da86e..6d09722e5cdbccf4cd146259475530dab83dc460 100755 GIT binary patch delta 3637 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 delta 2991 zcmZWr2{=^i8$UC~8ZwM^5}`4SeaTLd>`X+Mi(#xqg(&yt4hoa4a!L_G_9ZT|=XNdG zN@J;;B|_p}gd`OCpE+}T{*QaldCv1Y@B97U-}`>wcb@aTyZwl|9t023iUq|502}}i znXGV&2hVZ^0__wQhSehp!=f|;Kz;-P#-j*%7=sXUFsA?G??NcT_69BP@W?jUziI}aywisov+v}%DU zB>uvuiRQlL3F^kjwC(ScFD!r;_wXG75@NYRo(BC@vkD}QI_?B|B3aTUDc^DZWf-=+=I@ z`p%6j#=fOX?d<)xw#_mPH0=WY|?Sxusqt8R!@EN{RnAdb;_+pb~+L*!NK>7 z-u~mwU(8UauD$G@dw&zRaGf};=4db=pOfECcStKIJ*iG}LS3Hqws%lKXint^d?u7& z{)+v|5i|21N;oD=8^68S{Ig0#C2`4XxsMSZQ;TFapm(@u7uCbb1 zj8tPBCX28|^Jjd*@k42)v2AA(I`i?RxQio}_c7FtH2u}rfihot-m38^4wq#(xxW56 z#EDc7-WnXoird|vJ1c*Lr>8=Yp#LelVTmC+qyhnbXNVK)v_)=uXJ4^HuF94Ihe zMfXv!XGOU-IX!y>77}aQ(L*~z`53~H$5k=W5l%wQE;@|~wghKH*!$oNgdQDLr`a8l zT)J_;Hz#i{dMy5pbW_9Zuyx;Kb#mi^N->oSCyQRaPP3A+d5#n0>;8Rtyh7&Lt5c~` za!&=xcP|EZ zo_%`3eBQg4@Nra}Km4JaF-Db>ebr$0O27Q=Dp8V5=2KNpqjzSUMpJ$WtL0NB{3+^7 zRF9Q18iD`ObOvow=BF->7tPz=wMCAKkM0c}2~mz~a++Kh+y#7NgbXv!x}6^+h9#~?T0SBF zDHFC^-NPL|&sve8V`C#pHCEC3jwoN%8Oy5j&47&OEk|s2 z$CG4O2mu!+-p*C)ouy}HAF*Oxs~I{6M-YuT8n^x$o}xW4whGB@I~Bwpp=IQH~J z9+$tepGivpY+&GO)(7#s10wJB!t^IeI&N1hYW-HmXcLC96URQY49$lesiu8=_t`d2 zuZZ4En=pu-Fws&l7XHHTClPEJ;75JLHAUqz;?NLi4$396D(}!x-#JopCFjMW6BR$c zYeHX+SulIYV|#RwB;3Ng9EM4GNNe)rZbf4R%HC*l1t1Sc`ovP4`GOxe?0l?l^HeyO zc{IR-ZkORRgI9ihF%m%I9*?uPb=wVNN%8H)I{x0fp zgHEwL`=5C9mz_bPZx+U9KVlqR3xuV6cQtjFA5JxPai|G|8(pw<4m5zT8j|(zew1wHKws+c_XN zr4hLZY^p?ElXh)CX#8#Ztg}J1W+cU#gcdP1xoP+?CoC)ZkKG?9CI{ohH2JiBQz?`M z0QNyA-QgdnTusWpEEdLLSfjLG4h6rs3Tz?Z7bn4W=8LT`0ttKUMJT)#!Po?syp0JfYR=|u#4=j^UshTjHB)bFp7B? zq8U~1=^(%W4@q#$gCAzBcwmvAIY1t=x+H^G`BJ-$F}-qj0FZ_@;m4I9UCIrT$*M4^ z>h!c>JpusUAOS%32Lr!KiD6;%R?II&L)RsX0u^Wh^+VV_0b(MlUd&!f z!3&G5lK?He6yO}C?@KFLp>iKXbCcK;vMmh`;4xr{mncl+-T!?5GgbiDg3^iXAsXT! zq8PaC#SKqp9|eolRRCWoNr9FWrppC#Q5C^(3JzvGfXvUTU<*YSR`!`6Vmk*-(HSa3 zbdR#2Hpt>F3&$vWW06fdppCxxe`d{EY`ZiI#YsaNhxWwl8-Xv6Ni)iT6aD`a{dXer zQHUrAZg?~Ip4$hDj5!X<`7jHNyyUz=gM^ErXYU$O%)kpi%$_s{iou&hrWp9Aj|iNP z)s22B5L#DgcmB11#nuo}6vX>7^BMSJk#=Qh#*ZsM+Rz0YAjt|5tyrOd2>{4Iw=AR@;mWZ7 E2b1}(vH$=8 diff --git a/code/HeronLX.jar b/code/HeronLX.jar index 0445f9c712379acae4ce4499a2b08a9095cc2f02..145f480757192cea433b50366cc80e7fa57c6b2e 100755 GIT binary patch delta 13687 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 C37yCQ delta 11122 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